/*
 * SLD Editor - The Open Source Java SLD Editor
 *
 * Copyright (C) 2016, SCISYS UK Limited
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sldeditor.exportdata.esri;

import java.awt.image.Raster;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.sql.Blob;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.geotools.styling.FeatureTypeStyle;
import org.geotools.styling.NamedLayer;
import org.geotools.styling.Rule;
import org.geotools.styling.Style;
import org.geotools.styling.StyledLayer;
import org.geotools.styling.StyledLayerDescriptor;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.sldeditor.common.SLDData;
import com.sldeditor.common.SLDDataInterface;
import com.sldeditor.common.StyleWrapper;
import com.sldeditor.common.console.ConsoleManager;
import com.sldeditor.common.datasource.DataSourceField;
import com.sldeditor.common.datasource.DataSourceFieldInterface;
import com.sldeditor.common.datasource.DataSourcePropertiesInterface;
import com.sldeditor.common.output.SLDOutputFormatEnum;
import com.sldeditor.common.output.impl.SLDWriterFactory;
import com.sldeditor.exportdata.esri.datasource.DataSourceManager;
import com.sldeditor.exportdata.esri.keys.DatasourceKeys;
import com.sldeditor.exportdata.esri.label.EsriLabelRendererInterface;
import com.sldeditor.exportdata.esri.label.LabelEngineLayerProperties;
import com.sldeditor.exportdata.esri.renderer.EsriRendererInterface;
import com.sldeditor.exportdata.esri.renderer.SimpleRenderer;
import com.sldeditor.exportdata.esri.renderer.UniqueValueRenderer;
import com.sldeditor.exportdata.esri.symbols.SymbolUtils;
import com.vividsolutions.jts.geom.Geometry;

/**
 * Class that reads the intermediate Json file containing Esri MXD data and converts it to SLD data.
 * 
 * @author Robert Ward (SCISYS)
 */
public class MXDParser
{
    /** The renderer map. */
    private Map<String, EsriRendererInterface> rendererMap = new HashMap<String, EsriRendererInterface>();

    /** The label renderer map. */
    private Map<String, EsriLabelRendererInterface> labelRendererMap = new HashMap<String, EsriLabelRendererInterface>();

    /** The field type map. */
    private static Map<String, Class<?> > fieldTypeMap = new HashMap<String, Class<?> >();

    /** The logger. */
    private static Logger logger = Logger.getLogger(MXDParser.class);

    /**
     * Default constructor
     */
    private MXDParser()
    {
        initialise();
    }

    /**
     * Initialise.
     */
    private void initialise()
    {
        registerRenderers();
        registerLabelRenderers();

        if(fieldTypeMap.isEmpty())
        {
            fieldTypeMap.put("esriFieldTypeSmallInteger", Short.class);
            fieldTypeMap.put("esriFieldTypeInteger", Integer.class);
            fieldTypeMap.put("esriFieldTypeSingle", Float.class);
            fieldTypeMap.put("esriFieldTypeDouble", Double.class);
            fieldTypeMap.put("esriFieldTypeString", String.class);
            fieldTypeMap.put("esriFieldTypeDate", Date.class);
            fieldTypeMap.put("esriFieldTypeOID", Integer.class);
            fieldTypeMap.put("esriFieldTypeGeometry", Geometry.class);
            fieldTypeMap.put("esriFieldTypeBlob", Blob.class);
            fieldTypeMap.put("esriFieldTypeRaster", Raster.class);
            fieldTypeMap.put("esriFieldTypeGUID", String.class);
            fieldTypeMap.put("esriFieldTypeGlobalID", String.class);
        }
    }

    /**
     * Read layers from intermediate mxd files.
     *
     * @param mxdJSON the mxd JSON
     * @param outputFormat the output format
     * @return the MXD info
     */
    public static Map<String, SLDDataInterface> readLayers(String filename, SLDOutputFormatEnum outputFormat)
    {
        if(filename == null)
        {
            return null;
        }

        File intermediateFile = new File(filename);
        if(!intermediateFile.exists())
        {
            return null;
        }

        MXDParser parser = new MXDParser();

        MXDInfo mxdInfo = new MXDInfo();
        mxdInfo.setIntermediateFile(intermediateFile);
        JsonObject mxdJSON = convertFileToJSON(filename);

        // Read mxd filename
        String mxdFilename = "";
        JsonElement mxdFilenameJson = mxdJSON.get("mxd");
        if(mxdFilenameJson != null)
        {
            mxdFilename = mxdFilenameJson.getAsString();
            mxdInfo.setMxdFilename(mxdFilename);
        }

        File f = new File(mxdFilename);

        String mxdName = f.getName();
        int pos = mxdName.lastIndexOf("\\");
        if (pos > 0) {
            mxdName = mxdName.substring(pos + 1);
        }
        mxdInfo.setMxdName(mxdName);

        // Read layers
        Map<String, SLDDataInterface> layerMap = new HashMap<String, SLDDataInterface>();

        // Read layers
        JsonArray layerList = mxdJSON.getAsJsonArray("layers");

        for(int index = 0; index < layerList.size(); index ++)
        {
            JsonObject layer = (JsonObject) layerList.get(index);

            JsonElement layerNameElement = layer.get("name");
            if(layerNameElement != null)
            {
                String layerName = layerNameElement.getAsString();

                SLDDataInterface sldData = parser.importLayer(layer, outputFormat);

                layerMap.put(layerName, sldData);
            }
        }

        return layerMap;
    }

    /**
     * Convert file to json.
     *
     * @param fileName the file name
     * @return the json object
     */
    private static JsonObject convertFileToJSON(String fileName)
    {
        // Read from File to String
        JsonObject jsonObject = new JsonObject();

        try {
            JsonParser parser = new JsonParser();
            JsonElement jsonElement = parser.parse(new FileReader(fileName));
            jsonObject = jsonElement.getAsJsonObject();
        } catch (FileNotFoundException e) {
            ConsoleManager.getInstance().exception(MXDParser.class, e);
        }

        return jsonObject;
    }
    /**
     * Import layer.
     *
     * @param layer the layer
     * @param outputFormat the output format
     * @return the styled layer descriptor
     */
    private SLDDataInterface importLayer(JsonObject layer, SLDOutputFormatEnum outputFormat) {

        StyledLayerDescriptor sld = null;
        String layerName = layer.get("name").getAsString();

        double minScale = layer.get("minScale").getAsDouble();
        double maxScale = layer.get("maxScale").getAsDouble();

        int transparency = layer.get("transparency").getAsInt();

        JsonElement renderElement = layer.get("renderer");
        sld = getRenderer(layerName, minScale, maxScale, transparency, renderElement);

        JsonElement labelRenderArrayElement = layer.get("labelRenderers");
        if(labelRenderArrayElement != null)
        {
            processLabelRenderer(sld, labelRenderArrayElement.getAsJsonArray(), transparency);
        }

        JsonElement fieldArray = layer.get("fields");
        List<DataSourceFieldInterface> fieldList = processFields(layerName, fieldArray);

        JsonElement dataSourcePropertiesElement = layer.get("dataSource");
        DataSourcePropertiesInterface dataSourceProperties = processDataSource(layerName, dataSourcePropertiesElement);

        String sldContents = SLDWriterFactory.createWriter(outputFormat).encodeSLD(sld);

        StyleWrapper styleWrapper = new StyleWrapper(layerName, layerName);
        SLDDataInterface sldData = new SLDData(styleWrapper, sldContents);
        sldData.setDataSourceProperties(dataSourceProperties);
        sldData.setFieldList(fieldList);
        sldData.setReadOnly(true);

        return sldData;
    }

    /**
     * Process fields.
     *
     * @param layerName the layer name
     * @param fieldArrayElement the field array element
     * @return the list
     */
    private List<DataSourceFieldInterface> processFields(String layerName, JsonElement fieldArrayElement)
    {
        List<DataSourceFieldInterface> fieldList = new ArrayList<DataSourceFieldInterface>();

        if(fieldArrayElement != null)
        {
            JsonArray fieldArray = fieldArrayElement.getAsJsonArray();

            for(int index = 0; index < fieldArray.size(); index ++)
            {
                JsonObject fieldObject = null;

                try
                {
                    fieldObject = fieldArray.get(index).getAsJsonObject();
                }
                catch(IllegalStateException e)
                {
                    ConsoleManager.getInstance().error(this, "Layer : " + layerName);
                    ConsoleManager.getInstance().exception(this, e);
                }

                if(fieldObject != null)
                {
                    Class<?> fieldType = convertFieldType(fieldObject.get("type").getAsString());

                    DataSourceField esriField = new DataSourceField(fieldObject.get("field").getAsString(),
                            fieldType);

                    fieldList.add(esriField);
                }
            }
        }

        return fieldList;
    }

    /**
     * Process fields.
     *
     * @param layerName the layer name
     * @param dataSourcePropertiesElement the data source properties element
     * @return the data source properties
     */
    private DataSourcePropertiesInterface processDataSource(String layerName, JsonElement dataSourcePropertiesElement)
    {
        Map<String, String> propertyMap = new LinkedHashMap<String, String>();

        if(dataSourcePropertiesElement != null)
        {
            JsonObject dsObj = dataSourcePropertiesElement.getAsJsonObject();

            JsonElement typeElement = dsObj.get(DatasourceKeys.TYPE);
            if(typeElement != null)
            {
                propertyMap.put(DatasourceKeys.TYPE, typeElement.getAsString());
            }

            JsonElement pathElement = dsObj.get(DatasourceKeys.PATH);
            if(pathElement != null)
            {
                propertyMap.put(DatasourceKeys.PATH, pathElement.getAsString());
            }

            JsonObject properties = dsObj.getAsJsonObject(DatasourceKeys.PROPERTIES);

            if(properties != null)
            {
                for(Map.Entry<String, JsonElement> field : properties.entrySet())
                {
                    propertyMap.put(field.getKey(), field.getValue().getAsString());
                }
            }
        }

        DataSourcePropertiesInterface dataSourceProperties = DataSourceManager.getInstance().convert(propertyMap);

        return dataSourceProperties;
    }

    /**
     * Convert field type from String to a Class type.
     *
     * @param fieldType the field type
     * @return the class type
     */
    private static Class<?> convertFieldType(String fieldType)
    {
        if(fieldTypeMap.containsKey(fieldType))
        {
            return fieldTypeMap.get(fieldType);
        }
        return null;
    }

    /**
     * Gets the renderer.
     *
     * @param layerName the layer name
     * @param minScale the min scale
     * @param maxScale the max scale
     * @param transparency the transparency
     * @param renderElement the render element
     * @return the renderer
     */
    private StyledLayerDescriptor getRenderer(
            String layerName, double minScale, double maxScale, int transparency,
            JsonElement renderElement)
    {
        StyledLayerDescriptor sld = null;
        if(renderElement != null)
        {
            JsonObject renderer = renderElement.getAsJsonObject();

            boolean found = false;
            for(String rendererType : rendererMap.keySet())
            {
                JsonElement obj = renderer.get(rendererType);

                if(obj != null)
                {
                    EsriRendererInterface esriRenderer = rendererMap.get(rendererType);
                    sld = esriRenderer.convert(obj.getAsJsonObject(), layerName, minScale, maxScale, transparency);
                    found = true;
                    break;
                }
            }

            if(!found)
            {
                ConsoleManager.getInstance().error(this, "Unsupported renderer : " + SymbolUtils.extractName(renderElement));
            }
        }
        return sld;
    }

    /**
     * Gets the label renderer.
     *
     * @param sld the sld
     * @param labelRenderArrayElement the label render array element
     * @param transparency the transparency
     */
    private void processLabelRenderer(StyledLayerDescriptor sld, JsonArray labelRenderArrayElement, int transparency)
    {
        if(sld != null)
        {
            if(labelRenderArrayElement != null)
            {
                List<StyledLayer> styledLayers = sld.layers();

                if(!styledLayers.isEmpty())
                {
                    if(styledLayers.get(0) instanceof NamedLayer)
                    {
                        NamedLayer namedLayer = (NamedLayer)styledLayers.get(0);

                        List<Style> styleList = namedLayer.styles();

                        if(!styleList.isEmpty())
                        {
                            List<FeatureTypeStyle> ftsList = styleList.get(0).featureTypeStyles();
                            if(!ftsList.isEmpty())
                            {
                                List<Rule> ruleList = ftsList.get(0).rules();
                                List<Rule> newLabelRuleList = new ArrayList<Rule>();

                                for(int index = 0; index < labelRenderArrayElement.size(); index ++)
                                {
                                    JsonElement labelRenderElement = labelRenderArrayElement.get(index);

                                    JsonObject renderer = labelRenderElement.getAsJsonObject();

                                    boolean found = false;
                                    for(String labelRendererType : labelRendererMap.keySet())
                                    {
                                        JsonElement obj = renderer.get(labelRendererType);

                                        if(obj != null)
                                        {
                                            EsriLabelRendererInterface esriLabelRenderer = labelRendererMap.get(labelRendererType);
                                            if(esriLabelRenderer != null)
                                            {
                                                for(Rule rule : ruleList)
                                                {
                                                    esriLabelRenderer.convert(newLabelRuleList, rule, obj, transparency);
                                                }
                                            }

                                            found = true;
                                            break;
                                        }
                                    }

                                    if(!found)
                                    {
                                        ConsoleManager.getInstance().error(this, "Unsupported label renderer : " + SymbolUtils.extractName(labelRenderElement));
                                    }
                                }

                                ruleList.addAll(newLabelRuleList);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Register renderer converters.
     */
    private void registerRenderers() {

        logger.info("Renderers supported:");

        addRenderer(new SimpleRenderer());
        addRenderer(new UniqueValueRenderer());
    }

    /**
     * Adds the renderer.
     *
     * @param rendererObj the renderer obj
     */
    private void addRenderer(EsriRendererInterface rendererObj)
    {
        logger.info("\t" + rendererObj.getName());
        rendererMap.put(rendererObj.getName(), rendererObj);
    }

    /**
     * Adds the label renderer.
     *
     * @param labelRendererObj the label renderer obj
     */
    private void addLabelRenderer(EsriLabelRendererInterface labelRendererObj)
    {
        logger.info("\t" + labelRendererObj.getName());
        labelRendererMap.put(labelRendererObj.getName(), labelRendererObj);
    }

    /**
     * Register label renderer converters.
     */
    private void registerLabelRenderers() {
        logger.info("Label Renderers supported:");
        addLabelRenderer(new LabelEngineLayerProperties());
    }

}

