/*
 * Copyright 2018 TFI Systems

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

 * http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 */

package com.galfins.gnss_compare.DataViewers;

import android.location.Location;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridLayout;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import com.galfins.gnss_compare.CalculationModule;
import com.galfins.gnss_compare.CalculationModulesArrayList;
import com.galfins.gnss_compare.Constellations.Constellation;
import com.galfins.gnss_compare.R;
import com.google.common.collect.Sets;

/**
 * Created by Mateusz Krainski on 04/05/2018.
 * This is the main data viewer, which displays data in a text form.
 */
public class MainViewer extends Fragment implements DataViewer {

    double nameColumnWidth = 0.0;
    double itemColumnWidth = 0.0;

    private final String TAG = this.getClass().getSimpleName();

    /**
     * CalculationGridItem is used as an interface for dynamically updated items within a GridLayout
     */
    private interface CalculationGridItem {
        /**
         * Creates views and stores them into the layout
         * @param gridLayout Layout to which the views are to be assigned
         */
        void reinitializeViews(GridLayout gridLayout);

        /**
         * decrements row number on which this item is stored. Used when another item (above current one)
         * is removed from the grid layout
         */
        void decremntRowId();

        /**
         * @return row id of item
         */
        int getRowId();

        /**
         * Removes item from grid
         * @param grid layout to which the item was assigned
         */
        void removeFromGrid(GridLayout grid);

        void update(CalculationModule calculationModule);
    }

    /**
     * Item (row) in the constellation grid layout
     */
    private class ConstellationItem {

        private final static int POSE_NAME_COLUMN = 0;
        private final static int POSE_VISIBLE_COLUMN= 1;
        private final static int POSE_USED_COLUMN = 2;

        /**
         * Width of the text fields on this grid
         */
        private final int []TEXT_FIELD_WIDTH = {
                (int) nameColumnWidth,
                (int) itemColumnWidth,
                (int) itemColumnWidth,
                (int) itemColumnWidth,
        };

        /**
         * view storing the name of the calculation module
         */
        private TextView nameView;

        /**
         * view storing the number of visible satellites
         */
        private TextView visibleView;

        /**
         * View storing the number of used satellites
         */
        private TextView usedView;

        /**
         * row id associated with this item.
         */
        private int rowId;

        /**
         * Updates the views with current values from the {@code constellationReference}
         */
        private void updateViews(Constellation constellationReference) {

            if(nameView != null &&
                    visibleView != null &&
                    usedView != null) {

                nameView.setText(constellationReference.getName());
                visibleView.setText(String.format("%d", constellationReference.getVisibleConstellationSize()));
                usedView.setText(String.format("%d", constellationReference.getUsedConstellationSize()));
            }
        }

        /**
         * Updates the views with current values from the {@code constellationReference}
         */
        private void initializeViewsAsEmpty() {

            if(nameView != null &&
                    visibleView != null &&
                    usedView != null) {

                nameView.setText("--");
                visibleView.setText("--");
                usedView.setText("--");
            }
        }

        /**
         *  @param gridLayout layout to which the item is to be assigned to
         * @param rowId id of the row to which the item is to be assigned.
         */
        public ConstellationItem(GridLayout gridLayout, int rowId){

            this.rowId = rowId;
            reinitializeViews(gridLayout);
        }

        public void reinitializeViews(GridLayout gridLayout) {
            if(getActivity() != null) {

                removeFromGrid(gridLayout);

                nameView = new TextView(getActivity());
                visibleView = new TextView(getActivity());
                usedView = new TextView(getActivity());

                initializeTextView(nameView, gridLayout, POSE_NAME_COLUMN);
                initializeTextView(visibleView, gridLayout, POSE_VISIBLE_COLUMN);
                initializeTextView(usedView, gridLayout, POSE_USED_COLUMN);

                initializeViewsAsEmpty();
            }
        }

        /**
         * Factory method to initialize each text view and add it to the layout
         * @param view text view to be initialized
         * @param layout layout to which the view is to be added
         * @param columnId column of the layout to which the view is to be added
         */
        private void initializeTextView(
                TextView view,
                GridLayout layout,
                int columnId){

            layout.addView(view);

            GridLayout.LayoutParams param = new GridLayout.LayoutParams();
            param.height = GridLayout.LayoutParams.WRAP_CONTENT;
            param.width = GridLayout.LayoutParams.WRAP_CONTENT;
            param.rightMargin = 5;
            param.topMargin = 5;
            view.setTextSize(11);
            view.setWidth(TEXT_FIELD_WIDTH[columnId]);
            view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
            param.setGravity(Gravity.CENTER);
            param.columnSpec = GridLayout.spec(columnId);
            param.rowSpec = GridLayout.spec(rowId);
            view.setLayoutParams (param);
        }

        public void removeFromGrid(GridLayout grid) {
            grid.removeView(nameView);
            grid.removeView(visibleView);
            grid.removeView(usedView);
        }
    }

    private class ConstellationGrid{

        GridLayout referenceToLayout;
        Map<String, ConstellationItem> items = new HashMap<>();
        boolean initialized = false;

        private void initialize(){

            // todo: assess better if this race condition is true
            if(Constellation.getRegistered().size() > 0) {
                for (String key : Constellation.getRegistered()) {
                    try {
                        Constellation constellation = Constellation.getClassByName(key).newInstance();
                        referenceToLayout.setRowCount(referenceToLayout.getRowCount() + 1);
                        items.put(constellation.getName(), new ConstellationItem(referenceToLayout, items.size() + 1));
                    } catch (java.lang.InstantiationException | IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
                initialized = true;
            }
        }

        private void clearGrid(){
            for(Map.Entry<String, ConstellationItem> item : items.entrySet())
                item.getValue().removeFromGrid(referenceToLayout);

            items.clear();
        }

        public ConstellationGrid(GridLayout layout){
            referenceToLayout = layout;

            initialize();
        }

        public void reinitialize(GridLayout constellationGridView) {
            clearGrid();

            referenceToLayout = constellationGridView;

            initialize();
        }

        public void update(CalculationModulesArrayList calculationModules){

            if(!initialized)
                initialize();
            else {
                ConstellationItem item;
                for (CalculationModule calculationModule : calculationModules) {
                    item = items.get(calculationModule.getConstellation().getName());

                    if (item != null)
                        item.updateViews(calculationModule.getConstellation());
                }
            }

        }

    }

    /**
     * Item in the pose grid
     */
    private class PoseItem implements CalculationGridItem {

        private final static int POSE_NAME_COLUMN = 0;
        private final static int POSE_LAT_COLUMN = 1;
        private final static int POSE_LON_COLUMN = 2;
        private final static int POSE_ALT_COLUMN = 3;
        private final static int POSE_CLOCK_BIAS_COLUMN = 4;

        private final int []TEXT_FIELD_WIDTH = {
                (int) nameColumnWidth,
                (int) itemColumnWidth,
                (int) itemColumnWidth,
                (int) itemColumnWidth,
                (int) itemColumnWidth
        };

        private TextView nameView;
        private TextView latView;
        private TextView lonView;
        private TextView altView;
        private TextView clockBiasView;

        private int rowId;

        public PoseItem(GridLayout gridLayout, int rowId){

            this.rowId = rowId;

            reinitializeViews(gridLayout);
        }

        @Override
        public void reinitializeViews(GridLayout gridLayout){
            if(getActivity() != null) {

                removeFromGrid(gridLayout);

                nameView = new TextView(getActivity());
                latView = new TextView(getActivity());
                lonView = new TextView(getActivity());
                altView = new TextView(getActivity());
                clockBiasView = new TextView(getActivity());

                initializeTextView(nameView, gridLayout, POSE_NAME_COLUMN);
                initializeTextView(latView, gridLayout, POSE_LAT_COLUMN);
                initializeTextView(lonView, gridLayout, POSE_LON_COLUMN);
                initializeTextView(altView, gridLayout, POSE_ALT_COLUMN);
                initializeTextView(clockBiasView, gridLayout, POSE_CLOCK_BIAS_COLUMN);
            }
        }

        private void initializeTextView(
                TextView view,
                GridLayout layout,
                int columnId){

            layout.addView(view);

            GridLayout.LayoutParams param = new GridLayout.LayoutParams();
            param.height = GridLayout.LayoutParams.WRAP_CONTENT;
            param.width = GridLayout.LayoutParams.WRAP_CONTENT;
            param.rightMargin = 5;
            param.topMargin = 5;
            view.setTextSize(11);
            view.setWidth(TEXT_FIELD_WIDTH[columnId]);
            view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
            param.setGravity(Gravity.CENTER);
            param.columnSpec = GridLayout.spec(columnId);
            param.rowSpec = GridLayout.spec(rowId);
            view.setLayoutParams (param);
        }

        @Override
        public void decremntRowId(){
            rowId--;
        }

        @Override
        public int getRowId() {
            return rowId;
        }

        @Override
        public void removeFromGrid(GridLayout grid) {
            grid.removeView(nameView);
            grid.removeView(latView);
            grid.removeView(lonView);
            grid.removeView(altView);
            grid.removeView(clockBiasView);
        }

        @Override
        public void update(CalculationModule calculationModule) {

            //todo: throws error here when executed from incorrect thread?
            //todo: can be causing viewers crash?
            if (nameView != null &&
                    latView != null &&
                    lonView != null &&
                    altView != null &&
                    clockBiasView != null) {

                nameView.setText(calculationModule.getName());
                latView.setText(String.format("%.5f", calculationModule.getPose().getGeodeticLatitude()));
                lonView.setText(String.format("%.5f", calculationModule.getPose().getGeodeticLongitude()));
                altView.setText(String.format("%.1f", calculationModule.getPose().getGeodeticHeight()));
                clockBiasView.setText(String.format("%.0f", calculationModule.getClockBias()));

            }
        }
    }


    private GridLayout poseGridView;
    private GridLayout constellationGridView;
    private HashMap<CalculationModule, CalculationGridItem> poseItems = new HashMap<>();
    private ConstellationGrid constellationGrid;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(
                R.layout.main_viewer, container, false);

        nameColumnWidth = getResources().getDimension(R.dimen.name_column_width);
        itemColumnWidth = getResources().getDimension(R.dimen.item_column_width);

        poseGridView = rootView.findViewById(R.id.pose_list);
        constellationGridView = rootView.findViewById(R.id.constellation_list);

        if(constellationGrid == null)
            constellationGrid = new ConstellationGrid(constellationGridView);
        else
            constellationGrid.reinitialize(constellationGridView);

        return rootView;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        redrawGrid(poseGridView, poseItems);
    }

    private void redrawGrid(GridLayout grid, HashMap<CalculationModule, CalculationGridItem> items){

        grid.setRowCount(items.size() + 1);

        for(Map.Entry<CalculationModule, CalculationGridItem> entry: items.entrySet()) {
            entry.getValue().reinitializeViews(grid);
        }
    }

    private void removeSeriesFromGrid(
            CalculationModule calculationModule,
            GridLayout grid,
            Map<CalculationModule, CalculationGridItem> items) {

        int removedRowId = items.get(calculationModule).getRowId();

        for(Map.Entry<CalculationModule, CalculationGridItem> entry : items.entrySet()){
            if(entry.getValue().getRowId()>removedRowId) {
                entry.getValue().decremntRowId();
                entry.getValue().removeFromGrid(grid);
            }
        }

        items.get(calculationModule).removeFromGrid(grid);
        items.remove(calculationModule);

        // opposite order than in redrawGrid
        for(Map.Entry<CalculationModule, CalculationGridItem> entry: items.entrySet()) {
            entry.getValue().reinitializeViews(grid);
        }

        grid.setRowCount(items.size() + 1);
    }

    @Override
    public void onLocationFromGoogleServicesResult(Location location) {

    }

    private List<CalculationModule> modulesToBeAdded = new ArrayList<>();
    private List<CalculationModule> modulesToBeRemoved = new ArrayList<>();

    @Override
    public void update(CalculationModulesArrayList calculationModules) {

        synchronized (this) {
            modulesToBeAdded.clear();
            modulesToBeRemoved.clear();

            modulesToBeAdded.addAll(
                    Sets.difference(
                            new HashSet<>(calculationModules),
                            poseItems.keySet()
                    )
            );

            modulesToBeRemoved.addAll(
                    Sets.difference(
                            poseItems.keySet(),
                            new HashSet<>(calculationModules)
                    )
            );
        }
    }

    @Override
    public void updateOnUiThread(CalculationModulesArrayList calculationModules) {

        if(constellationGrid==null || poseItems == null)
            return;

        constellationGrid.update(calculationModules);

        for(CalculationModule calculationModule : calculationModules) {
            if (poseItems.containsKey(calculationModule)) {
                try {
                    // update sometimes throws CalledFromWrongThreadException
                    poseItems.get(calculationModule).update(calculationModule);
                } catch (Exception e){
                    Log.e(TAG, "update: Exception thrown" );
                    e.printStackTrace();
                }
            }
        }

        synchronized (this) {

            for (CalculationModule calculationModule : modulesToBeAdded) {
                poseGridView.setRowCount(poseGridView.getRowCount() + 1);
                poseItems.put(calculationModule, new PoseItem(
                        poseGridView,
                        poseGridView.getRowCount() - 1
                ));
                poseItems.get(calculationModule).update(calculationModule);
            }
            modulesToBeAdded.clear();

            for (CalculationModule calculationModule : modulesToBeRemoved) {
                removeSeriesFromGrid(calculationModule, poseGridView, poseItems);
            }
            modulesToBeRemoved.clear();
        }
    }
}