#-----------------------------------------------------
# Author: Yaqiang Wang
# Date: 2015-9-20
# Purpose: MeteoInfoLab layer module
# Note: Jython
#-----------------------------------------------------
import geoutil
import mipylib.miutil as miutil
import mipylib.numeric as np
from java.awt import Font
from java.time import LocalDateTime

from org.meteoinfo.data import TableUtil, XYListDataset
from org.meteoinfo.geo.layer import LayerTypes, VectorLayer
from org.meteoinfo.geometry.legend import LegendType
from org.meteoinfo.projection import ProjectionUtil, KnownCoordinateSystems
from org.meteoinfo.geometry.shape import PolygonShape, ShapeTypes
from org.meteoinfo.geo.analysis import GeometryUtil
from org.meteoinfo.geo.util import GeoProjectionUtil

class MILayer(object):
    """
    Map layer
    
    :param layer: (*MapLayer*) MapLayer object.
    :param shapetype: (*ShapeTypes*) Shape type ['point' | 'point_z' | 'line' | 'line_z' | 'polygon'
        | 'polygon_z']
    """
    def __init__(self, layer=None, shapetype=None):
        if layer is None:
            if shapetype is None:
                print('shapetype must be specified!')
            else:
                shapetype = shapetype.upper()
                if shapetype == 'LINE':
                    shapetype = 'POLYLINE'
                elif shapetype == 'LINE_Z':
                    shapetype = 'POLYLINE_Z'
                try:
                    type = ShapeTypes.valueOf(shapetype)
                except:
                    print('shapetype is not valid: {}'.foramt(shapetype))
                    type = ShapeTypes.POINT
                self._layer = VectorLayer(type)
                self.shapetype = type
                self.proj = KnownCoordinateSystems.geographic.world.WGS1984
        else:
            self._layer = layer
            self.shapetype = layer.getShapeType()
            self.proj = layer.getProjInfo()
        self._coord_array = None
    
    def __repr__(self):
        return self._layer.getLayerInfo()

    @property
    def x_coord(self):
        """
        Get X coordinate array.
        :return: X coordinate array
        """
        if self.isvectorlayer():
            if self._coord_array is None:
                self._coord_array = GeometryUtil.getCoordinates(self._layer)
            return np.array(self._coord_array[0])
        else:
            return None

    @property
    def y_coord(self):
        """
        Get Y coordinate array.
        :return: Y coordinate array
        """
        if self.isvectorlayer():
            if self._coord_array is None:
                self._coord_array = GeometryUtil.getCoordinates(self._layer)
            return np.array(self._coord_array[1])
        else:
            return None

    @property
    def z_coord(self):
        """
        Get Z coordinate array.
        :return: Z coordinate array
        """
        if self.isvectorlayer():
            if self._coord_array is None:
                self._coord_array = GeometryUtil.getCoordinates(self._layer)
            return np.array(self._coord_array[2])
        else:
            return None

    @property
    def m_coord(self):
        """
        Get M coordinate array.
        :return: M coordinate array
        """
        if self.isvectorlayer():
            if self._coord_array is None:
                self._coord_array = GeometryUtil.getCoordinates(self._layer)
            return np.array(self._coord_array[3])
        else:
            return None
    
    def isvectorlayer(self):
        """
        Check this layer is VectorLayer or not.
        
        :returns: (*boolean*) Is VectorLayer or not.
        """
        return self._layer.getLayerType() == LayerTypes.VECTOR_LAYER
        
    def get_encoding(self):
        """
        Get encoding.
        
        :returns: (*string*) Encoding
        """
        return self._layer.getAttributeTable().getEncoding()
        
    def gettable(self):
        """
        Get attribute table.
        
        :returns: (*PyTableData') Attribute table.
        """
        r = self._layer.getAttributeTable().getTable()
        return np.datatable(r)
    
    def cellvalue(self, fieldname, shapeindex):
        """
        Get attribute table cell value.
        
        :param fieldname: (*string*) Field name.
        :param shapeindex: (*int*) Shape index.
        
        :returns: The value in attribute table identified by field name and shape index.
        """
        v = self._layer.getCellValue(fieldname, shapeindex)
        if isinstance(v, LocalDateTime):
            dt = miutil.pydate(v)
            return dt
        else:
            return v
            
    def setcellvalue(self, fieldname, shapeindex, value):
        """
        Set cell value in attribute table.
        
        :param fieldname: (*string*) Field name.
        :param shapeindex: (*int*) Shape index.
        :param value: (*object*) Cell value to be asigned.
        """
        self._layer.editCellValue(fieldname, shapeindex, value)
            
    def shapes(self):
        """
        Get shapes.
        """
        return self._layer.getShapes()
        
    def shapenum(self):
        """
        Get shape number
        """
        return self._layer.getShapeNum()
        
    def legend(self):
        """
        Get legend scheme.
        """
        return self._layer.getLegendScheme()
        
    def setlegend(self, legend):
        """
        Set legend scheme.
        
        :param legend: (*LegendScheme*) Legend scheme.
        """
        self._layer.setLegendScheme(legend)
        
    def update_legend(self, ltype, fieldname):
        """
        Update legend scheme.
        
        :param ltype: (*string*) Legend type [single | unique | graduate].
        :param fieldname: (*string*) Field name.
        """
        if ltype == 'single':
            ltype = LegendType.SINGLE_SYMBOL
        elif ltype == 'unique':
            ltype = LegendType.UNIQUE_VALUE
        elif ltyp == 'graduate':
            ltype = LegendType.GRADUATED_COLOR
        else:
            raise ValueError(ltype)
        self._layer.updateLegendScheme(ltype, fieldname)
        return self._layer.getLegendScheme()
    
    def addfield(self, fieldname, dtype, values=None):
        """
        Add a field into the attribute table.
        
        :param fieldname: (*string*) Field name.
        :param dtype: (*string*) Field data type [string | int | float | double].
        :param values: (*array_like*) Field values.
        """
        dt = TableUtil.toDataTypes(dtype)
        self._layer.editAddField(fieldname, dt)
        if not values is None:
            n = self.shapenum()
            for i in range(n):
                if i < len(values):
                    self._layer.editCellValue(fieldname, i, values[i])
                    
    def delfield(self, fieldname):
        """
        Delete a field from the attribute table.
        
        :param fieldname: (*string*) Filed name.
        """
        self._layer.editRemoveField(fieldname)
        
    def renamefield(self, fieldname, newfieldname):
        """
        Rename the field.
        
        :param fieldname: (*string*) The old field name.
        :param newfieldname: (*string*) The new field name.
        """
        self._layer.editRenameField(fieldname, newfieldname)
        
    def addshape(self, x, y, fields=None, z=None, m=None):
        """
        Add a shape.
        
        :param x: (*array_like*) X coordinates of the shape points.
        :param y: (*array_like*) Y coordinates of the shape points.
        :param fields: (*array_like*) Field values of the shape.
        :param z: (*array_like*) Optional, Z coordinates of the shape points.
        :param m: (*array_like*) Optional, M coordinates of the shape points.
        """
        shapes = geoutil.makeshapes(x, y, self.shapetype, z, m)
        if len(shapes) == 1:
            self._layer.editAddShape(shapes[0], fields)
        else:
            for shape, field in zip(shapes, fields):
                self._layer.editAddShape(shape, field)

    def copy(self):
        """
        Copy the layer.

        :return: (*MILayer*) Copied layer.
        """
        return MILayer(layer=self._layer.clone())

    def move(self, xshift=0, yshift=0):
        """
        Move shapes.

        :param xshift: (*float*) X shift.
        :param yshift: (*float*) Y shift.
        """
        self._layer.move(xshift, yshift)
                    
    def addlabels(self, fieldname, **kwargs):
        """
        Add labels
        
        :param fieldname: (*string*) Field name
        :param fontname: (*string*) Font name. Default is ``Arial``.
        :param fontsize: (*string*) Font size. Default is ``14``.
        :param bold: (*boolean*) Font bold or not. Default is ``False``.
        :param color: (*color*) Label color. Default is ``None`` with black color.
        :param xoffset: (*int*) X coordinate offset. Default is ``0``.
        :param yoffset: (*int*) Y coordinate offset. Default is ``0``.
        :param avoidcoll: (*boolean*) Avoid labels collision or not. Default is ``True``.
        :param decimals: (*int*) Number of decimals of labels.
        """
        labelset = self._layer.getLabelSet()
        labelset.setFieldName(fieldname)
        fontname = kwargs.pop('fontname', 'Arial')
        fontsize = kwargs.pop('fontsize', 14)
        bold = kwargs.pop('bold', False)
        if bold:
            font = Font(fontname, Font.BOLD, fontsize)
        else:
            font = Font(fontname, Font.PLAIN, fontsize)
        labelset.setLabelFont(font)
        color = kwargs.pop('color', None)
        if not color is None:
            color = miutil.getcolor(color)
            labelset.setLabelColor(color)
        xoffset = kwargs.pop('xoffset', 0)
        labelset.setXOffset(xoffset)
        yoffset = kwargs.pop('yoffset', 0)
        labelset.setYOffset(yoffset)
        avoidcoll = kwargs.pop('avoidcoll', True)
        labelset.setAvoidCollision(avoidcoll)
        decimals = kwargs.pop('decimals', None)
        if not decimals is None:
            labelset.setAutoDecimal(False)
            labelset.setDecimalDigits(decimals)
        self._layer.addLabels()
        
    def getlabel(self, text):
        """
        Get a label.
        
        :param text: (*string*) The label text.
        """
        return self._layer.getLabel(text)
        
    def movelabel(self, label, x=0, y=0):
        """
        Move a label.
        
        :param label: (*string*) The label text.
        :param x: (*float*) X shift for moving in pixel unit.
        :param y: (*float*) Y shift for moving in pixel unit.
        """
        self._layer.moveLabel(label, x, y)
        
    def add_charts(self, fieldnames, legend=None, **kwargs):
        """
        Add charts
        
        :param fieldnames: (*list of string*) Field name list.
        :param legend: (*LegendScheme*) Chart legend.
        :param charttype: (*string*) Chart type [bar | pie]. Default is ``bar``.
        :param minsize: (*int*) Minimum chart size. Default is ``0``.
        :param maxsize: (*int*) Maximum chart size. Default is ``50``.
        :param barwidth: (*int*) Bar width. Only valid for bar chart. Default is ``8``.
        :param xoffset: (*int*) X coordinate offset. Default is ``0``.
        :param yoffset: (*int*) Y coordinate offset. Default is ``0``.
        :param avoidcoll: (*boolean*) Avoid labels collision or not. Default is ``True``.
        :param align: (*string*) Chart align type [center | left | right | none], Default is ``center``.
        :param view3d: (*boolean*) Draw chart as 3D or not. Default is ``False``.
        :param thickness: (*int*) 3D chart thickness. Default is ``5``.
        :param drawlabel: (*boolean*) Draw label or not. Default is ``False``.
        :param fontname: (*string*) Label font name.
        :param fontsize: (*int*) Label font size.
        :param bold: (*boolean*) Font bold or not. Default is ``False``.
        :param labelcolor: (*color*) Label color.
        :param decimals: (*int*) Number of decimals of labels.
        """
        charttype = kwargs.pop('charttype', None)
        minsize = kwargs.pop('minsize', None)
        maxsize = kwargs.pop('maxsize', None)
        barwidth = kwargs.pop('barwidth', None)
        xoffset = kwargs.pop('xoffset', None)
        yoffset = kwargs.pop('yoffset', None)
        avoidcoll = kwargs.pop('avoidcoll', None)
        align = kwargs.pop('align', None)
        view3d = kwargs.pop('view3d', None)
        thickness = kwargs.pop('thickness', None)
        drawlabel = kwargs.pop('drawlabel', None)
        fontname = kwargs.pop('fontname', 'Arial')
        fontsize = kwargs.pop('fontsize', 12)
        bold = kwargs.pop('bold', False)
        if bold:
            font = Font(fontname, Font.BOLD, fontsize)
        else:
            font = Font(fontname, Font.PLAIN, fontsize)
        labelcolor = kwargs.pop('labelcolor', None)
        decimals = kwargs.pop('decimals', None)
        
        chartset = self._layer.getChartSet()
        chartset.setFieldNames(fieldnames)
        chartset.setLegendScheme(legend)
        if not charttype is None:
            chartset.setChartType(charttype)
        if not minsize is None:
            chartset.setMinSize(minsize)
        if not maxsize is None:
            chartset.setMaxSize(maxsize)
        if not barwidth is None:
            chartset.setBarWidth(barwidth)
        if not xoffset is None:
            chartset.setXShift(xoffset)
        if not yoffset is None:
            chartset.setYShift(yoffset)
        if not avoidcoll is None:
            chartset.setAvoidCollision(avoidcoll)
        if not align is None:
            chartset.setAlignType(align)
        if not view3d is None:
            chartset.setView3D(view3d)
        if not thickness is None:
            chartset.setThickness(thickness)
        if not drawlabel is None:
            chartset.setDrawLabel(drawlabel)
        chartset.setLabelFont(font)
        if not labelcolor is None:
            chartset.setLabelColor(miutil.getcolor(labelcolor))
        if not decimals is None:
            chartset.setDecimalDigits(decimals)
        self._layer.updateChartSet()
        self._layer.addCharts()
        return chartset
        
    def get_chartlegend(self):
        """
        Get legend of the chart graphics.
        """
        return self._layer.getChartSet().getLegendScheme()
        
    def get_chart(self, index):
        """
        Get a chart graphic.
        
        :param index: (*int*) Chart index.
        
        :returns: Chart graphic
        """
        return self._layer.getChartPoints()[index]
        
    def move_chart(self, index, x=0, y=0):
        """
        Move a chart graphic.
        
        :param index: (*int*) Chart index.
        :param x: (*float*) X shift for moving.
        :param y: (*float*) Y shift for moving.
        """
        s = self._layer.getChartPoints()[index].getShape()
        p = s.getPoint()
        p.X = p.X + x
        p.Y = p.Y + y
        s.setPoint(p)
        
    def set_avoidcoll(self, avoidcoll):
        """
        Set if avoid collision or not. Only valid for VectorLayer with Point shapes.
        
        :param avoidcoll: (*boolean*) Avoid collision or not.
        """
        self._layer.setAvoidCollision(avoidcoll)
        
    def project(self, toproj):
        """
        Project to another projection.
        
        :param toproj: (*ProjectionInfo*) The projection to be projected.
        """
        GeoProjectionUtil.projectLayer(self._layer, toproj)
        
    def buffer(self, dist=0, merge=False):
        """
        Get the buffer layer.
        
        :param dist: (*float*) Buffer value.
        :param merge: (*boolean*) Merge the buffered shapes or not.
        
        :returns: (*MILayer*) Buffered layer.
        """
        r = self._layer.buffer(dist, False, merge)
        return MILayer(r)
        
    def clip(self, clipobj):
        """
        Clip this layer by polygon or another polygon layer.
        
        :param clipobj: (*PolygonShape or MILayer*) Clip object.
        
        :returns: (*MILayer*) Clipped layer.
        """
        if isinstance(clipobj, PolygonShape):
            clipobj = [clipobj]
        elif isinstance(clipobj, MILayer):
            clipobj = clipobj._layer
        r = self._layer.clip(clipobj)
        return MILayer(r)
        
    def select(self, expression, seltype='new'):
        """
        Select shapes by SQL expression.
        
        :param expression: (*string*) SQL expression.
        :param seltype: (*string*) Selection type ['new' | 'add_to_current' |
            'remove_from_current' | 'select_from_current']
            
        :returns: (*list of Shape*) Selected shape list.
        """
        self._layer.sqlSelect(expression, seltype)
        return self._layer.getSelectedShapes()
        
    def clear_selection(self):
        """
        Clear shape selection.
        """
        self._layer.clearSelectedShapes()
        
    def clone(self):
        """
        Clone self.
        """
        return MILayer(self._layer.clone())
    
    def save(self, fn=None, encoding=None):
        """
        Save layer as shape file.
        
        :param fn: (*string*) Shape file name (.shp).
        :param encoding: (*string*) Encoding.
        """
        if fn is None:
            fn = self._layer.getFileName()
            
        if fn.strip() == '':
            print('File name is needed to save the layer!')
            raise IOError
        else:
            if encoding is None:
                self._layer.saveFile(fn)
            else:
                self._layer.saveFile(fn, encoding)
    
    def savekml(self, fn):
        """
        Save layer as KML file.
        
        :param fn: (*string*) KML file name.
        """
        self._layer.saveAsKMLFile(fn)

    def savebil(self, fn, proj=None):
        """
        Save layer as bil file.

        :param fn: (*str*) Bil file name.
        :param proj: (*ProjectionInfo*) Projection. Default is None.
        """
        if proj is None:
            self._layer.saveFile(fn)
        else:
            self._layer.saveFile(fn, proj)


class MIXYListData():
    def __init__(self, data=None):
        if data is None:
            self.data = XYListDataset()
        else:
            self.data = data
        
    def __getitem__(self, indices):
        if not isinstance(indices, tuple):
            inds = []
            inds.append(indices)
            indices = inds
            
        if isinstance(indices[0], int):
            if isinstance(indices[1], int):
                x = self.data.getX(indices[0], indices[1])
                y = self.data.getY(indices[0], indices[1])
                return x, y
            else:
                return self.data.getXValues(indices[0]), self.data.getXValues(indices[0])           
        
    def size(self, series=None):
        if series is None:
            return self.data.getSeriesCount()
        else:
            return self.data.getItemCount(series)
            
    def addseries(self, xdata, ydata, key=None):
        if key is None:
            key = 'Series_' + str(self.size())
        if isinstance(xdata, list):
            self.data.addSeries(key, xdata, ydata)
        else:
            self.data.addSeries(key, xdata.asarray(), ydata.asarray())   
            