"""
Module contains classes that can build configuration strings for particular frameworks.

Every framework should have its own class that handles configuration generation.
"""
from datetime import datetime
import copy

__all__ = ['Cavelib']

class AbstractBuilder:
    """
    Predecessor for all builders.

    No logic/algorithm is stored here, everything is framework-dependent.
    """
    def __init__(self, environment, computers, master, baseconfig, customization):
        """
        Assigns fields.

        The overriding mechanism has to be defined when implementing particular controller configuration builder.
        :param environment: Environment settings. See the environment module and its model.configuration method
        :param computers: Dictionary with all computers that will participate in the application run. Again, see environment module. The computer's attributes are taken from there.
        :param master: Single dictionary representing the node chosen as a master. In fact it should be one record from computers parameter (But it has to be present in computers as well!).
        :param baseconfig: Basic configuration that the framework configuration uses. {attrname: value}, framework dependent.
        :param customization: You can overwrite baseconfig attributes with this dictionary.
        """
        self.base = baseconfig
        self.override = customization
        self.master = master
        self.environment = environment
        self.computers = computers

    def build(self):
        """
        This is framework-specific and should always be implemented.

        :rtype: dictionary {computername: configurationAsString}
        """
        return ""

class Cavelib(AbstractBuilder):
    """
    Framework configuration for CAVELib framework.

    http://www.mechdyne.com/cavelib.aspx


    """
    def _override(self, base):
        """
        Applies values from self.override.

        All attributes present in self.override are written into the base dictionary.
        Old values are always overwritten.
        """
        for name, value in self.override.iteritems():
            base[name] = value
        return base

    def _apply_environment(self, base):
        """
        Applies values from self.environment.

        As the environment dictionary has different structure, we can not use the _override method.
        If any param in self.environment has `value`, it is used, if not `default` field is used.
        No other fields are used here.
        Old values are always overwritten in base.
        """
        for param in self.environment:
            base[param['name']] = param['value'] if 'value' in param else param['default']
        return base

    def _general(self, data):
        """
        Transforms attributes common for all computers into a configuration string.

        Currently, it can properly process these parameters (presented as keys in `data`):
          - origin (as in Origin - originLeftDistance, originFrontDistance, originRightDistance, originFloorDistance)
          - eyes (as in WallEyes - eyesLeftWall, eyesRightWall, eyesFrontWall, eyesFloorWall)
          - displayMode
          - interocularDistance
          - units
          - trackerType
          - caveWidth
          - caveHeight
          - simulator
          - cluster

        Attributes with names: right, left, floor, front (wall representations) are skipped here (see _generate_projections).
        If the value of the attribute is an empty string, it is ignored as well.
        :param data: dictionary {attrname: value}
        """
        ret = ['# Generated by claun']
        origin = {}
        eyes = {}
        for (name, value) in data.iteritems():
            if name in ['right', 'left', 'floor', 'front'] or value == u'':
                continue
            elif name.startswith('origin'):
                origin[name[6:]] = value
            elif name.startswith('eyes'):
                eyes[name[4:-4].lower()] = value
            elif name == 'displayMode':
                ret.append( 'DisplayMode ' + ('Stereo' if value is True else 'Mono') )
            elif name == 'interocularDistance':
                try:
                    ret.append( 'InterocularDistance %i cm' % float(value) )
                except ValueError:
                    ret.append( 'InterocularDistance ' + value )
            elif name == 'units':
                conversion = {"m": "meters", "f": "feet", "i": "inches", "cm": "centimeters"}
                if value in conversion:
                    v = conversion[value]
                else:
                    v = 'feet'
                ret.append( 'Units ' + v)
            elif (name == 'trackerType' and value == 'none') or (name == 'controllerType' and value == 'none'):
                continue
            elif name == 'caveWidth' or name == 'caveHeight':
                ret.append( name[0:4].upper() + name[4:] + ' ' + value)
            elif name == 'simulator':
                ret.append( 'Simulator ' + ('y' if value is True else 'n'))
            elif name != 'cluster':
                correctname = name[0:1].upper() + name[1:]
                ret.append( correctname + ' ' + value)

        #origin
        ret.append( 'Origin ' + \
                str(origin['LeftDistance']) + ' ' + \
                str(origin['FloorDistance']) + ' ' + \
                str(origin['FrontDistance']) + ' ' + \
                str(origin['Units']))

        #eyes
        for k, l in eyes.iteritems():
            ret.append( 'WallEyes ' + k + ' ' + l )

        return "\n".join(ret)

    def _generate_master(self, data):
        """
        Returns DistribTCPMaster row.

        `data` has to contain an ip key
        """
        return 'DistribTCPMaster ' + data['ip']

    def _generate_computer(self, data, ismaster, lowestpriority = 1):
        """
        Generates machine dependent configuration options including DistribID, ControllerType and TrackerType.

        ControllerType and TrackerType are set to simulator if the machine is not a master machine
        (The general approach is that the master machine should take care of the controller).
        DistribID is based on the `masterpriority` attribute that has to be present as key in data dictionary
        :param data: Dictionary with information about the machine
        :param ismaster: Boolean value if the computer is deemed to be a master node
        :param lowestpriority: To ensure correct DistribID numbering (starting with zero), we have to pass the lowest available priority to the method (optional, default is 1)
        """
        s =  'DistribID ' + str(int(data['masterpriority']) - lowestpriority) + '\n'
        if not ismaster:
            s += 'ControllerType simulator\n'
            s += 'TrackerType simulator\n'
        return s

    def _generate_cluster(self, data):
        """
        Generates the DistribNodes attribute.
        """
        return 'DistribNodes ' + str(len(data))

    def _generate_projections(self, projections, allowed):
        """
        Generates information about all given `projections` which includes WallDisplay and Wall parameter.

        :param projections: Dictionary containing information about projections where key is always the projection name and value (dict) has to contain following attributes:
          - xscreen - Screen for the XServer
          - xmax - maximum position on the x axis
          - xmin - minimum position on the x axis
          - ymax - maximum position on the y axis
          - ymin - minimum position on the y axis
        """
        walls = []
        r = ''
        for name, wall in projections.iteritems():
            if name not in allowed: continue
            r = r + 'WallDisplay ' + name + ' :'+ str(wall['xscreen']) + ' ' + str(wall['xmax']) + 'x' + str(wall['ymax']) + '+' + str(wall['xmin']) + '+' + str(wall['ymin']) + '\n'
            walls.append(name)
        rx = 'Walls ' + ' '.join(walls) + '\n'
        return rx + r

    def build(self):
        """
        Prepares configuration strings for all computers in self.computers.

        All input dictionaries should contain the key 'parameters'. No other fields are reflected in this builder.
        The key parameters should be a dictionary where each key is a name of one parameter and its value is its value.

        Firstly a deepcopy of the base['parameters'] is created and then self._override and self._apply_environment are
        applied **in that order** (environment may thus overwrite the override settings).

        Then the general parameters are generated, master, cluster and finally projections for each computer (That's the bit where
        the configurations differ).

        :return: Dict {hostname: configurationstring}
        """
        configs = {}
        self.derived = copy.deepcopy(self.base['parameters'])
        self.derived  = self._override(self.derived)
        self.derived = self._apply_environment(self.derived)

        output = [self._general(self.derived)]
        output.append( self._generate_master(self.master) )
        output.append(self._generate_cluster(self.computers))

        common = "\n".join(output)

        activeprojections = [p['name'] for p in self.environment if 'group' in p and p['group'] == 'projection' and (('value' in p and p['value']) or ('value' not in p and p['default']))]
        masterpriorities = sorted([c['masterpriority'] for c in self.computers.itervalues()])

        for m in self.computers.itervalues():
            projections = self._generate_projections(m['projections'], activeprojections)
            machine = self._generate_computer(m, m['hostname'] == self.master['hostname'], masterpriorities[0])
            configs[m['hostname']] = "\n".join([common, projections, machine])

        self.product = configs
        return self.product


if __name__ == "__main__":
    # Cavelib testing ground
    base = {u'parameters': {u'originLeftDistance': u'1.2', u'originUnits': u'meters', u'originFloorDistance': u'-0.6', u'serialTracking': u'n', u'caveHeight': u'1.8 meters', u'eyesLeftWall': u'both', u'eyesRightWall': u'both', u'calibration': u'n', u'interocularDistance': u'6 cm', u'controllerDaemonKey': u'4127', u'defaultTrackerOrientation': u'', u'trackerDaemonKey': u'4126', u'units': u'feet', u'hideCursor': u'y', u'stereoBuffer': u'y', u'originFrontDistance': u'1.2', u'calibrationFile': u'', u'appDistribution': u'tcp', u'defaultTrackerPosition': u'', u'eyesFrontWall': u'both', u'tracking': u'y', u'distribTcpPort': u'7112', u'caveWidth': u'2.4 meters', u'eyesFloorWall': u'both', u'distribution': u'tcp'}, u'_rev': u'1-be37fa8bd11695217a5fa639a4c4dc63', u'framework_name': u'cavelib', u'configuration_name': u'default', u'_id': u'19c6c10a2f9feb136de7cd0286000f01', u'type': u'framework'}
    computers = [{u'masterpriority': 1, 'platform': u'ubuntu', 'monitors': {u'MemoryLoadMonitor': {'description': u'Memory usage', 'value': u'39%'}, u'CpuLoadMonitor': {'description': u'Average load', 'value': u'1.07'}, u'CpuTempMonitor': {'description': u'CPU temperature', 'value': u'58.0 \xb0C'}}, u'ip': u'127.0.1.1', u'hostname': u'phobos', u'fqdn': u'phobos.claun.cz', u'port': 34122, u'platforms': [u'gentoo', u'ubuntu'], u'connected': True, u'projections': {u'front': {u'xmin': 0, u'xmax': 800, u'ymin': 0, u'ymax': 600, u'xscreen': u'0.0'}, u'right': {u'xmin': 0, u'xmax': 800, u'ymin': 0, u'ymax': 600, u'xscreen': u'0.1'}}, 'online': True, 'hwcontrol': [u'restart-x', u'reboot', u'shutdown'], u'id': u'phobos'}]
    master = {u'masterpriority': 1, 'platform': u'ubuntu', 'monitors': {u'MemoryLoadMonitor': {'description': u'Memory usage', 'value': u'39%'}, u'CpuLoadMonitor': {'description': u'Average load', 'value': u'1.07'}, u'CpuTempMonitor': {'description': u'CPU temperature', 'value': u'58.0 \xb0C'}}, u'ip': u'127.0.1.1', u'hostname': u'phobos', u'fqdn': u'phobos.claun.cz', u'port': 34122, u'platforms': [u'gentoo', u'ubuntu'], u'connected': True, u'projections': {u'front': {u'xmin': 0, u'xmax': 800, u'ymin': 0, u'ymax': 600, u'xscreen': u'0.0'}, u'right': {u'xmin': 0, u'xmax': 800, u'ymin': 0, u'ymax': 600, u'xscreen': u'0.1'}}, 'online': True, 'hwcontrol': [u'restart-x', u'reboot', u'shutdown'], u'id': u'phobos'}
    environment = [{u'user_editable': True, u'name': u'displayMode', u'default': True, u'human_name': u'Stereo?', u'type': u'boolean', u'id': u'displayMode', u'help': u'If on, the application will render images in 3D (If supported).'}, {u'user_editable': True, u'name': u'floor', u'default': False, u'human_name': u'Floor Projection', u'type': u'boolean', u'id': u'floor', u'group': u'projection', u'help': u'The images may not appear on this projection wall.'}, {u'user_editable': True, u'name': u'front', u'default': True, u'human_name': u'Front Projection', u'type': u'boolean', u'id': u'front', u'group': u'projection', u'help': u'The images may not appear on this projection wall.'}, {u'user_editable': True, u'name': u'interocularDistance', u'min': 0, u'default': 6, u'max': 30, u'human_name': u'Interocular distance', u'type': u'slider', u'id': u'interocularDistance', u'help': u'Default distance between eyes in centimeters. This may affect the 3D effect.'}, {u'user_editable': True, u'name': u'left', u'default': False, u'human_name': u'Left Projection', u'type': u'boolean', u'id': u'left', u'group': u'projection', u'help': u'The images may not appear on this projection wall.'}, {u'user_editable': False, u'name': u'originLeftDistance', u'min': 0, u'default': 1.2, u'max': 4, u'human_name': u'Left wall distance', u'type': u'slider', u'id': u'originLeftDistance', u'help': u'Left wall distance from the center of the projection (in meters).'}, {u'user_editable': True, u'name': u'right', u'default': False, u'human_name': u'Right Projection', u'type': u'boolean', u'id': u'right', u'group': u'projection', u'help': u'The images may not appear on this projection wall.'}, {u'user_editable': True, u'name': u'units', u'default': u'f', u'values': [u'm', u'f', u'i'], u'human_name': u'Units', u'type': u'dropdown', u'id': u'units', u'help': u'In which units will CAVE compute.'}]
    override = {u'interocularDistance': u'12 cm'}
    c = Cavelib(environment, computers, master, base, override)
    print( c.build()['phobos'] )
