import clr, sys, os

# --- System Imports ---

import System
from System import Array, Byte
from System.Collections.Generic import List

# --- Ansys Imports ---

clr.AddReference("Ans.Utilities")
clr.AddReference("Ans.UI.Toolkit")
clr.AddReference("Ans.UI.Toolkit.Base")

import Ansys.Utilities
import Ansys.UI.Toolkit
import Ansys.UI.Toolkit.Drawing

from Ansys.Utilities import Color
from Ansys.Utilities import Palette

# --- SceneGraph Imports ---

scenGraphDir = Ansys.Utilities.ApplicationConfiguration.DefaultConfiguration.AwpRootEnvironmentVariableValue
scenGraphDir += '/Addins/SceneGraphChart/bin/'
scenGraphDir += Ansys.Utilities.ApplicationConfiguration.DefaultConfiguration.Platform
sys.path.append(os.path.normpath(scenGraphDir))

clr.AddReferenceToFile("Ans.SceneGraphChart.dll")
clr.AddReferenceToFile("Ans.SceneGraphChart.Interop.dll")

import Ans.SceneGraphChart as SceneGraph
from Ans.SceneGraphChart.Components import IErrorHandler, LinePattern, PointPattern, VariableStyle, FillPattern, LogBase, AxisPair
from Ans.SceneGraphChart.Properties import PointSize

# --------------------------------------
#   Map / Entry / Collection - Classes
# --------------------------------------

class _Map():
    '''
    Map for linking short str with long str, and long
    str with enum.
    Ex : 'b' -> 'blue' -> Palette.Blue
    '''

    def __init__(self, entries=[]):
        self.short_str_2_long_str = {}
        self.long_str_2_value = {}
        for entry in entries:
            self.append(*entry)

    def append(self, value, long_str, short_str=None):
        self.long_str_2_value[long_str] = value
        if short_str: self.short_str_2_long_str[short_str] = long_str

    def get_value(self, from_value):
        if type(from_value) == str:
            if from_value in self.short_str_2_long_str.iterkeys():
                return self.long_str_2_value[self.short_str_2_long_str[from_value]]
            if from_value in self.long_str_2_value.iterkeys():
                return self.long_str_2_value[from_value]
        return None    

class _Entry():
    '''
    Represent an entry.
    '''

    def __init__(self, name, property_name, value_type, default, map=None):
        self.name = name
        self.value_type = value_type
        self.property_name = property_name
        self.map = map
        self.default = self.get_value(default)

    def get_value(self, from_value):
        if from_value == None:
            return None
        if type(from_value) == self.value_type:
            return from_value
        if self.map != None:
            value = self.map.get_value(from_value)
            if value != None: return value
        return self.default

    def set_value(self, object, from_value):
        value = self.get_value(from_value)
        if value == None: return

        fullpath = self.property_name.split('.')
        for path in fullpath[:-1]: 
            object = object.__getattribute__(path)
        object.__setattr__(fullpath[-1], value)

    def get_value_from_short_combination(self, combination):
        if combination == None: return None
        if self.map != None:
            for short in self.map.short_str_2_long_str.iterkeys(): 
                if short in combination: 
                    return self.get_value(short)
        return None

class _Collection():
    '''
    Represent a collection of entries.
    '''

    def __init__(self, prefix, entriesrange=[]):
        self.prefix = prefix
        self.entries = {}
        for entry in entriesrange:
            self.append(*entry)

    def append(self, name, property_name, value_type, default, map=None):
        self.entries[self.prefix + name] = _Entry(name, property_name, value_type, default, map)

    def get_values(self, **kwargs):
        values = {}
        for name, entry in self.entries.iteritems():
            if name in kwargs:
                values[name] = entry.get_value(kwargs[name])    
            else:
                values[name] = entry.default
        return values

    def set_values(self, object, **kwargs):
        values = self.get_values(**kwargs)
        for name, value in values.iteritems():
            if name in self.entries:
                self.entries[name].set_value(object, value)

# --------------------------------------
#   Map / Entries - Declarations
# --------------------------------------

# --- Maps Declatation ---

_maps = {

    'color': _Map([
        (Palette.Blue   , 'blue'   , 'b'),
        (Palette.Green  , 'green'  , 'g'),
        (Palette.Red    , 'red'    , 'r'),
        (Palette.Cyan   , 'cyan'   , 'c'),
        (Palette.Magenta, 'magenta', 'm'),
        (Palette.Yellow , 'yellow' , 'y'),
        (Palette.Black  , 'black'  , 'k'),
        (Palette.White  , 'white'  , 'w'),
    ]),

    'linestyle': _Map([
        (LinePattern.LinePatternSolid      , 'solid'      , '-'  ),
        (LinePattern.LinePatternDashMedium , 'dash'       , '--' ),
        (LinePattern.LinePatternMedium     , 'dot'        , ':'  ),
        (LinePattern.LinePatternDashDot    , 'dashdot'    , '--.'),
        (LinePattern.LinePatternDashDashDot, 'dashdashdot', '-.' ),
        (LinePattern.LinePatternDashDotDot , 'dashdotdot' , '-..'),
    ]),

    'symbolstyle': _Map([
        (PointPattern.PointPatternNone    , 'none'),
        (PointPattern.PointPatternStar    , 'star'    , '*'),
        (PointPattern.PointPatternXCross  , 'xcross'  , 'x'),
        (PointPattern.PointPatternCross   , 'cross'   , '+'),
        (PointPattern.PointPatternDiamond , 'diamon'  , 'D'),
        (PointPattern.PointPatternTriangle, 'triangle', '^'),
        (PointPattern.PointPatternEllipse , 'circle'  , 'o'),
        (PointPattern.PointPatternHexagon , 'hexagon' , 'h'),
        (PointPattern.PointPatternRect    , 'rect'    , 'R'),
    ]),

    'fillstyle': _Map([
        (FillPattern.FillPatternNone         , 'none'),
        (FillPattern.FillPatternSolid        , 'solid'),
        (FillPattern.FillPatternDense        , 'dense'),
        (FillPattern.FillPatternMedium       , 'medium'),
        (FillPattern.FillPatternSparse       , 'sparse'),
        (FillPattern.FillPatternHorizontal   , 'horizontal', '-'),
        (FillPattern.FillPatternVertical     , 'vertical'  , '|'),
        (FillPattern.FillPatternCross        , 'cross'     , '+'),
        (FillPattern.FillPatternCrossDiagonal, 'xcross'    , 'x'),
    ]),

    'pointsize': _Map([
        (int(PointSize.Header), 'header'),
        (int(PointSize.Normal), 'normal'),
        (int(PointSize.Small) , 'small'),
        (int(PointSize.Title) , 'title'),
    ]),

    'variablestyle': _Map([
        (VariableStyle.VariableStyleNone   , 'none'),
        (VariableStyle.VariableStyleLine   , 'line'),
        (VariableStyle.VariableStyleSpline , 'spline'),
        (VariableStyle.VariableStyleBar    , 'bar'),
        (VariableStyle.VariableStyleStep   , 'step'),
        (VariableStyle.VariableStyleSurface, 'surface'),
    ]),

    'logbase': _Map([
        (LogBase.LogBaseNone   , 'none'),
        (LogBase.LogBaseNatural, 'natural'),
        (LogBase.LogBaseCommon , 'common'),
    ]),
         
}

# --- Entries Declaration ---

_collections = {

    'variable': _Collection('variable', [
        ('style'   , 'Style.Style' , VariableStyle, 'line', _maps['variablestyle']),
        ('pickable', 'Pickable'    , bool         , True  ),
        ('label'   , 'Label'       , str          , None  ),
    ]),

    'line': _Collection('line', [
        ('color'    , 'Color'    , Color        , 'blue' , _maps['color']),
        ('style'    , 'Style'    , LinePattern  , 'solid', _maps['linestyle']),
        ('width'    , 'Width'    , float        , 1.     ),
        ('antialias', 'AntiAlias', bool         , True   ),
    ]),

    'bar': _Collection('bar', [
        ('offset'  , 'Offset'  , float, None),
        ('width'   , 'Width'   , float, None),
        ('topsize' , 'TopSize' , float, None),
        ('topcolor', 'TopColor', Color, 'red', _maps['color']),
    ]),

    'fill': _Collection('fill', [
        ('background', 'Base' , Color      , None, _maps['color']),
        ('color'     , 'Color', Color      , None, _maps['color']),
        ('style'     , 'Style', FillPattern, None, _maps['fillstyle']),
    ]),

    'symbol': _Collection('symbol', [
        ('size'        , 'Size'        , float       , 10.    ),
        ('style'       , 'Style'       , PointPattern, 'none' , _maps['symbolstyle']),
        ('fillcolor'   , 'FillColor'   , Color       , 'blue' , _maps['color']),
        ('outlinecolor', 'OutlineColor', Color       , 'black', _maps['color']),
    ]),

    'text': _Collection('text', [
        ('background', 'Background', Color, 'white' , _maps['color']),
        ('color'     , 'Color'     , Color, 'black' , _maps['color']),
        ('font'      , 'Font'      , str  , None ),
        ('size'      , 'Size'      , int  , 'normal', _maps['pointsize']),
    ]),

    'axis': _Collection('axis', [
        ('log'    , 'Logrithmic', LogBase, None , _maps['logbase']),
        ('enabled', 'Enabled'   , bool   , False),
    ]),

    'grid': _Collection('grid', [
        ('color'    , 'Color'    , Color        , Palette.LightGray, _maps['color']),
        ('style'    , 'Style'    , LinePattern  , 'solid'          , _maps['linestyle']),
        ('width'    , 'Width'    , float        , 1.     ),
        ('antialias', 'AntiAlias', bool         , True   ),
    ]),
            
}

# --------------------------------------
#   Figures Class
# --------------------------------------

class Figures(Ansys.UI.Toolkit.TableLayoutPanel):

    def __init__(self, columns, rows, *figures):
        """
        Figures(columns, rows, fig1, fig2, ...)
            - columns : the number of columns.
            - rows : the number of rows.
            - fig1, ... : the figures to attach.
        """
        Ansys.UI.Toolkit.TableLayoutPanel.__init__(self)
        self.view = self
        self.figures = figures

        self.setfigures(columns, rows, *figures)

    # ------------------------
    #   methodes
    # ------------------------

    def setfigures(self, columns, rows, *figures):

        self.figures = figures

        self.view.SuspendLayout()

        self.view.Controls.Clear()
        self.view.Rows.Clear()
        self.view.Columns.Clear()

        if figures == None:
            self.view.ResumeLayout()
            return

        for i in range(columns): self.view.Columns.Add(Ansys.UI.Toolkit.TableLayoutSizeType.Percent, 100.0/columns)
        for i in range(rows)   : self.view.Rows.Add(Ansys.UI.Toolkit.TableLayoutSizeType.Percent, 100.0/rows)

        index = 0
        for row in range(rows):
            for column in range(columns):
                if index >= len(figures): self.view.Controls.Add(Ansys.UI.Toolkit.Panel(), row, column)
                if index <  len(figures): self.view.Controls.Add(figures[index], row, column)
                index += 1

        # Hack ! for 14.5
        self.hackfig = None
        if columns == 1 and rows == 1:
            self.hackfig = Figure()
            self.view.Rows.Add(Ansys.UI.Toolkit.TableLayoutSizeType.Absolute, 4.)
            self.view.Controls.Add(self.hackfig, rows, 0)

        self.view.ResumeLayout()

    def dispose(self):
        for figure in self.figures:
            figure.dispose()
        if self.hackfig != None:
            self.hackfig.dispose()
        self.view.Hide()
        self.view.Dispose()
        self.view = None

    def clear(self):
        for figure in self.figures:
            figure.clear()

# -----------------------------------
#   Figure Class
# -----------------------------------

class Figure(SceneGraph.Window):

    def __new__(cls, **kwargs):

        # Start the engine if needed
        global _engine
        _engine.start()
        
        # Call parent constructor
        self = SceneGraph.Window.__new__(cls)
        self.view = self

        # Parameters
        self._default_x_axis = kwargs.get('xaxis', 'bottom')
        self._default_y_axis = kwargs.get('yaxis', 'left')

        # Initialization
        self._figures = None

        self._factory = SceneGraph.Nodes.NodeFactory()

        self.viewport = self._factory.Create[SceneGraph.Nodes.Viewport]("ChartViewport")
        cap = self._factory.Create[SceneGraph.Core.SceneCapabilities]()
        self.view.Renderer.Bind(self.viewport, cap)
        
        self.chart = self._factory.Create[SceneGraph.Nodes.ChartXY]("Chart")
        self.chart.Crosshairs.Style = LinePattern.LinePatternNone

        self.axes = {
            'bottom': self._factory.Create[SceneGraph.Nodes.AxisContinuous]("bottom-axis"),
            'top'   : self._factory.Create[SceneGraph.Nodes.AxisContinuous]("top-axis"),
            'left'  : self._factory.Create[SceneGraph.Nodes.AxisContinuous]("left-axis"),
            'right' : self._factory.Create[SceneGraph.Nodes.AxisContinuous]("right-axis")
        }

        for axis in self.axes.values():
            self.set_axis(axis)
            self.set_grid(axis)

        self.chart.AxisBottom = self.axes['bottom']
        self.chart.AxisTop    = self.axes['top']
        self.chart.AxisLeft   = self.axes['left']
        self.chart.AxisRight  = self.axes['right']
        
        self.viewport.Attach(self.chart)
        self.legendViewport = None

        return self

    # ------------------------
    #   friendly access
    # ------------------------

    def dispose(self):
        if self.view == None: return
        self.view.Renderer.Unbind()
        self.view.Hide()
        self.view.Dispose()
        self.view = None

    def clear(self):
        self.chart.Clear()
        self.clearlegend()
        self._set_enable_all_axis(False)

    def clearlegend(self):
        if self.legendViewport != None:
            self.legendViewport.Clear()
            self.viewport.Detach(self.legendViewport)
            self.legendViewport = None

    def title(self, title, **kwargs):
        kwargs.setdefault('textsize', 'title')
        self.set_title(self.chart, title, **kwargs)

    def xlabel(self, label, **kwargs):
        self.axislabel(self._default_x_axis, label, **kwargs)

    def ylabel(self, label, **kwargs):
        self.axislabel(self._default_y_axis, label, **kwargs)

    def axislabel(self, axisname, label, **kwargs):
        kwargs.setdefault('textsize', 'header')
        self.set_label(self.axes[axisname], label, **kwargs)

    def bar(self, *args, **kwargs):
        kwargs['variablestyle'] = 'bar'    
        kwargs.setdefault('fillstyle', 'solid')
        kwargs.setdefault('fillcolor', 'red')
        self.plot(*args, **kwargs)

    def line(self, *args, **kwargs):
        kwargs['variablestyle'] = 'line'
        self.plot(*args, **kwargs)

    def spline(self, *args, **kwargs):
        kwargs['variablestyle'] = 'spline'
        self.plot(*args, **kwargs)

    def plot(self, *args, **kwargs):
        """
        Plot given data. Available input format:
        (x, y, shortstr) or (y, shortstr) or (x, y) or (y).
        Available entries : line|bar, variable, symbol, fill.
        """

        kwargs.setdefault('variablestyle', 'line')
        kwargs.setdefault('xaxis', self._default_x_axis)
        kwargs.setdefault('yaxis', self._default_y_axis)
 
        variable_properties = _collections['variable'].get_values(**kwargs)

        x, y, short_str = self._parse_args(*args)

        if variable_properties['variablestyle'] == VariableStyle.VariableStyleLine or \
           variable_properties['variablestyle'] == VariableStyle.VariableStyleSpline:

            linestyle = _collections['line'].entries['linestyle'].get_value_from_short_combination(short_str)
            linecolor = _collections['line'].entries['linecolor'].get_value_from_short_combination(short_str)
            symbolstyle = _collections['symbol'].entries['symbolstyle'].get_value_from_short_combination(short_str)
            symbolfillcolor = _collections['symbol'].entries['symbolfillcolor'].get_value_from_short_combination(short_str)
            
            if linestyle != None : kwargs['linestyle'] = linestyle
            if linecolor != None : kwargs['linecolor'] = linecolor
            if symbolstyle != None : kwargs['symbolstyle'] = symbolstyle
            if symbolfillcolor != None : kwargs['symbolfillcolor'] = symbolfillcolor

        # Create variable
        variable = self._factory.Create[SceneGraph.Nodes.VariableXY]("variableXY")

        if variable_properties['variablestyle'] == VariableStyle.VariableStyleLine or \
           variable_properties['variablestyle'] == VariableStyle.VariableStyleSpline:
            self.set_line(variable, **kwargs)
        elif variable_properties['variablestyle'] == VariableStyle.VariableStyleBar:
            self.set_bar(variable, **kwargs)
        else:
            return

        self.set_variable(variable, **kwargs)
        self.set_symbol(variable, **kwargs)
        self.set_fill(variable, **kwargs)

        self._set_enable_axis(kwargs['xaxis'], True)
        self._set_enable_axis(kwargs['yaxis'], True)
        variable.Axes = self._get_axis_pair(kwargs['xaxis'], kwargs['yaxis'])
            
        self._set_and_add_variable(variable, x, y)

        self.viewport.Touch()
        return variable

    def legend(self):

        self.clearlegend()

        legend = self._factory.Create[SceneGraph.Nodes.Legend]("Legend")
        legend.BarSide = SceneGraph.Components.BarSide.BarSideRightTop
        legend.Orientation = SceneGraph.Components.Orientation.OrientationHorizontal

        for i, variable in enumerate(self.chart.Variables):
            entry = self._factory.Create[SceneGraph.Nodes.LegendEntry]("Legend Entry {0}".format(i))

            if not variable.LabelIsDefined:
                continue

            entry.Label = variable.Label
            entry.LineStyle.Color = variable.Style.Line.Color
            entry.LineStyle.Base =variable.Style.Line.Base
            entry.LineStyle.Width = variable.Style.Line.Width
            entry.LineStyle.Style = variable.Style.Line.Style
            entry.SymbolStyle.FillColor = variable.Style.Symbol.FillColor
            entry.SymbolStyle.OutlineColor = variable.Style.Symbol.OutlineColor
            entry.SymbolStyle.Style = variable.Style.Symbol.Style

            if not variable.Style.Line.BaseIsDefined:
                  entry.LineStyle.Base = Ansys.Utilities.Color(Byte(0), Byte(0), Byte(0), Byte(0))

            legend.Entries.Attach(entry)

        legendVP = self._factory.Create[SceneGraph.Nodes.Viewport]("Legend VP")
        legendVP.Movable = False
        legendVP.Resizable = False
        legendVP.AllowProjectionChange = False
        legendVP.Background = Ansys.Utilities.Color(Byte(0), Byte(0), Byte(0), Byte(0))
        legendVP.Border = Ansys.Utilities.Color(Byte(0), Byte(0), Byte(0), Byte(0))
        legendVP.Dimensions = SceneGraph.Properties.RelativeViewport.FromFractionsTopLeft(0.06, 0.06, 0.7, 0.4);

        legendVP.Attach(legend)
        self.viewport.Attach(legendVP)
        self.legendViewport = legendVP

    # ------------------------
    #   setter
    # ------------------------

    def set_variable(self, variable, **kwargs):
        _collections['variable'].set_values(variable, **kwargs)

    def set_symbol(self, variable, **kwargs):
        _collections['symbol'].set_values(variable.Style.Symbol, **kwargs)

    def set_line(self, variable, **kwargs):
        _collections['line'].set_values(variable.Style.Line, **kwargs)

    def set_bar(self, variable, **kwargs):
        _collections['bar'].set_values(variable.Style.Bar, **kwargs)

    def set_fill(self, variable, **kwargs):
        _collections['fill'].set_values(variable.Style.Fill, **kwargs)

    def set_label(self, axis, label, **kwargs):
        axis.Label = label
        _collections['text'].set_values(axis.Text, **kwargs)

    def set_title(self, chart, label, **kwargs):
        chart.Label = label
        _collections['text'].set_values(chart.Text, **kwargs)

    def set_axis(self, axis, **kwargs):
        _collections['axis'].set_values(axis, **kwargs)

    def set_grid(self, axis, **kwargs):
        _collections['grid'].set_values(axis.Grid, **kwargs)

    # ------------------------
    #   privates
    # ------------------------

    def _set_enable_axis(self, axisname, enabled):
        self.axes[axisname].Enabled = enabled

    def _set_enable_all_axis(self, enabled):
        for axis in self.axes.values():
            axis.Enabled = enabled

    def _get_axis_pair(self, xaxis, yaxis):
        if xaxis == 'left' and yaxis == 'bottom': return AxisPair.AxisPairLeftBottom
        if xaxis == 'bottom' and yaxis == 'left': return AxisPair.AxisPairBottomLeft
        if xaxis == 'left' and yaxis == 'top': return AxisPair.AxisPairLeftTop
        if xaxis == 'top' and yaxis == 'left': return AxisPair.AxisPairTopLeft
        if xaxis == 'right' and yaxis == 'bottom': return AxisPair.AxisPairRightBottom
        if xaxis == 'bottom' and yaxis == 'right': return AxisPair.AxisPairBottomRight
        if xaxis == 'right' and yaxis == 'top': return AxisPair.AxisPairRightTop
        if xaxis == 'top' and yaxis == 'right': return AxisPair.AxisPairTopRight
        raise Exception('invalid axes combination')

    def _parse_args(self, *args):
        """
        Parse args with short code like (x, y, short) or (y, short).
        Parse x and/or y args like (x, y) or (y).
        """

        x, y, short_str = None, None, None

        if len(args) > 1 and type(args[-1]) == str:
            short_str = args[-1]
            args = args[:-1]
        elif len(args) == 3:
            raise ValueError('third arg must be a format string')

        if len(args) == 2:
            x = args[0]
            y = args[1]
        elif len(args) == 1:
            y = args[0]
            x = range(len(y))
        else:
            raise ValueError('invalid x and/or y input')

        return x, y, short_str

    def _set_and_add_variable(self, variable, x, y):

            xData = self._factory.Create[SceneGraph.Nodes.VariableData]("x-data") ; xData.Data = self._tofloatlist(x)
            yData = self._factory.Create[SceneGraph.Nodes.VariableData]("y-data") ; yData.Data = self._tofloatlist(y)

            variable.XData = xData
            variable.YData = yData

            self.chart.Variables.Attach(variable)

    def _tofloatlist(self, value):
        try: return List[float](Array[float](value))
        except: raise ValueError('x and/or y must be float arrays')    

# --------------------------------------
#   Handler
# --------------------------------------

class _ErrorHandler(IErrorHandler):

    def Error(self, p_message, p_details):
        System.Console.Error.WriteLine("Error: {0}\n{1}", p_message, p_details)

    def FatalError(self, p_message, p_details):
        System.Console.Error.WriteLine("Fatal Error: {0}\n{1}", p_message, p_details)

    def Information(self, p_message, p_details):
        System.Console.Error.WriteLine("Information: {0}\n{1}", p_message, p_details)

    def Warning(self, p_message, p_details):
        System.Console.Error.WriteLine("Warning: {0}\n{1}", p_message, p_details)

# --------------------------------------
#   Engine management
# --------------------------------------

class _Engine():

    def __init__(self):
        self._engine = None
        self._started = False
    
    def __del__(self):
        self.stop()
    
    # Starts the SceneEngine.
    def start(self):
        if not self._started:
            try:
                self._engine = SceneGraph.Engine(SceneGraph.SystemConfig(False), _ErrorHandler())
                self._started = True
            except:
                pass
    
    # Stops the SceneEngine.
    def stop(self):
        if self._started:
            self._started = False

# Create global engine
_engine = _Engine()
