#=============================================================================
#
#  Copyright (c) Kitware, Inc.
#  All rights reserved.
#  See LICENSE.txt for details.
#
#  This software is distributed WITHOUT ANY WARRANTY; without even
#  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
#  PURPOSE.  See the above copyright notice for more information.
#
#=============================================================================
from __future__ import print_function
import os
print('loading', os.path.basename(__file__))
import sys

import smtk
import smtk.attribute

# Workaround for missing enum
if not hasattr(smtk.attribute.Item, 'ReferenceType'):
  print('Adding smtk.attribute.Item.ReferenceType')
  smtk.attribute.Item.ReferenceType = 13

# ---------------------------------------------------------------------
class CardFormat:
  '''Formatter for each Truchas output line

  '''
  # ModelManager
  ModelManager = None

  # Shared/static conditions
  Conditions = set()

  # Use to number properties for namelists that use them
  PropertyIndex = 0


# ---------------------------------------------------------------------
  def __init__(self, keyword,
    as_boolean = False,
    as_property = False,
    att_type=None,
    expression_keyword = None,
    if_condition = None,
    is_custom = False,
    item_path=None,
    literal_value = None,
    set_condition = None,
    skip_default = False,
    use_condition_for_boolean = None,
    use_model_entities_for_value = False,
    use_name_for_value = False):
    '''Formatting object for project file output card

    Required argument:
    keyword: (string) text to write as the Truchas keyword. If None,
      no output will be written (can be used to set conditions)

    Optional arguments:
    as_boolean: (boolean) write ".true." or ".false." based on enabled state
    as_property: (boolean) write out using Truchas "property" syntax
    att_type: (string) attribute type for this card's info.
      This is typically provided by the writer.
    expression_keyword: (string) alternate keyword to use when item is an
      an expression (instead of a value)
    if_condition: (string) only write output if the condition is in the class'
      Conditions set. The if_condition argument can be an iterable,
      in which case, ALL elements must be in the class' Conditions set.
    is_custom: (boolean) placeholder for values using custom code
    item_path: (string) smtk "path" to item where info can be found
    literal_value: (string) Write hard-code value instead of item's value
    set_condition: (string) add condition in class Conditions set.
      Is NOT executed if the if_condition fails
    skip_default: (boolean) Only write value if not the item default
    use_condition_for_boolean: (string) write true/false based on the
      presence of the argument in the class' Conditions set.
    use_model_entities_for_value: (boolean) Write the list of side-set
      ids as the value. Should only be used for attribute associations,
      not other model item entities.
    use_name_for_value: (boolean) write the attribute or item name
      instead of its value
    '''
    self.keyword = keyword

    self.as_boolean = as_boolean
    self.as_property = as_property
    self.att_type = att_type
    self.expression_keyword = expression_keyword
    if isinstance(if_condition, set):
      self.if_condition = if_condition
    elif hasattr(if_condition, '__iter__'):
      self.if_condition = set(if_condition)
    elif if_condition is not None:
      self.if_condition = set([if_condition])
    else:
      self.if_condition = None
    self.is_custom = is_custom
    self.item_path = item_path
    self.literal_value = literal_value
    self.set_condition = set_condition
    self.skip_default = skip_default
    self.use_condition_for_boolean = use_condition_for_boolean
    self.use_model_entities_for_value = use_model_entities_for_value
    self.use_name_for_value = use_name_for_value

# ---------------------------------------------------------------------
  def write(self, out, att, base_item_path=None):
    '''Writes card for input attribute
    '''
    if self.is_custom:
      print('WARNING: Ignoring custom card for keyword', self.keyword)
      return False

    # Skip cards with conditions that don't match
    if not self.matches_conditions():
      return False

    if self.use_condition_for_boolean is not None:
      value = self.use_condition_for_boolean in self.__class__.Conditions
      self.__class__.write_value(out, self.keyword, value, as_boolean=True)
      return self._finish_write()

    if self.literal_value is not None:
      self.__class__.write_value(out, self.keyword, self.literal_value)
      return self._finish_write()

    # Handle attribute case first
    if self.item_path is None:
      return self.write_attribute(out, att)

    # Get the item
    full_item_path = self.item_path
    if base_item_path is not None:
      full_item_path = base_item_path
      if self.item_path:
        full_item_path = '/'.join([base_item_path, self.item_path])
    item = att.itemAtPath(full_item_path, '/')
    if item is None:
      print('ERROR: item not found for attribute %s path %s' % \
        (att.name(), full_item_path))
      return False

    if item.type() == smtk.attribute.Item.VoidType:
      self.__class__.write_value(
        out, self.keyword, item.isEnabled(), as_boolean=True)
      return self._finish_write()

    if not item.isEnabled():
      return False

    # Special cases
    if self.keyword is None:
      return self._finish_write()
    elif self.as_boolean:
      self.__class__.write_value(out, self.keyword, True, as_boolean=True)
      return self._finish_write()
    elif self.use_name_for_value:
      self.__class__.write_value(out, self.keyword, item.name())
      return self._finish_write()

    # If value isn't set, skip
    if not item.isSet(0):
      return False

    # Check skip_default option
    if self.skip_default and item.isUsingDefault(0):
      return False

    if item.type() == smtk.attribute.Item.ModelEntityType:
      id_string = self.__class__.get_model_entity_ids(item, as_string=True)
      if id_string:
        self.__class__.write_value(out, self.keyword, id_string, quote_string=False)
      return

    if item.type() == smtk.attribute.Item.ReferenceType:
      id_string = self.__class__.get_model_entity_ids(item, as_string=True)
      if id_string:
        self.__class__.write_value(out, self.keyword, id_string, quote_string=False)
      return

    if hasattr(item, 'numberOfValues') and \
      item.numberOfValues() > 1:
      value_list = list()
      for i in range(item.numberOfValues()):
        value_list.append(item.value(i))
      string_list = [str(x) for x in value_list]
      string_value = ', '.join(string_list)
      self.__class__.write_value(
        out, self.keyword, string_value, quote_string=False)
      return

    # (else) Single value or expression
    keyword = self.keyword
    #property_prefix = 'property_constant'
    value = 'undefined'
    is_expression = False
    if hasattr(item, 'isExpression') and item.isExpression(0):
      is_expression = True
      expression_att = item.expression(0)
      value = expression_att.name()
      if self.expression_keyword is not None:
        keyword = self.expression_keyword
    else:
      value = item.value(0)
      # RefItem a special case
      if hasattr(smtk, 'AttributePtr') and isinstance(value, smtk.AttributePtr):
        value = value.name()

    if self.as_property:
      self.write_property(out, value, is_expression)
    else:
      self.__class__.write_value(out, keyword, value)

    return self._finish_write()

# ---------------------------------------------------------------------
  def write_attribute(self, out, att):
    '''Writes card associated with attribute only (no item)

    '''
    if self.use_name_for_value:
      self.__class__.write_value(out, self.keyword, att.name())
    elif self.use_model_entities_for_value:
      id_string = self.__class__.get_associated_entity_ids(att, as_string=True)
      self.__class__.write_value(out, self.keyword, id_string, quote_string=False)

    return self._finish_write()

# ---------------------------------------------------------------------
  def write_property(self, out, value, is_expression=False):
    '''Writes value to output stream in the Truchas "property" format

    Gets the index from the class PropertyIndex variable
    '''
    index = self.__class__.PropertyIndex + 1
    name_string = 'property_name(%d) = \"%s\",' % (index, self.keyword)

    value_prefix = 'property_function' if is_expression else 'property_constant'
    if isinstance(value, basestring):
      value = '\"%s\"' % value
    predicate_string = ' %s(%d) = %s' % (value_prefix, index, value)

    tab = 35
    if len(name_string) > tab:
      tab = len(name_string)
    text_formatter = '  {:<%s}{:}\n' % tab
    line = text_formatter.format(name_string, predicate_string)
    out.write(line)
    self.__class__.PropertyIndex = index

# ---------------------------------------------------------------------
  def matches_conditions(self):
    '''Checks if_condition on this card

    '''
    if isinstance(self.if_condition, set):
      # ALL if_condition elements must be in class Conditions
      return self.if_condition <= self.__class__.Conditions
    # (else)
    return True

# ---------------------------------------------------------------------
  def _finish_write(self):
    '''Internal method for common code after value written. Returns True

    Currently that is checking the set_condition
    '''
    if self.set_condition is not None:
      self.__class__.Conditions.add(self.set_condition)
    return True

# ---------------------------------------------------------------------
  @classmethod
  def test_conditions(klass, test_condition):
    '''Checks test_conditions with class Conditions

    '''
    # Default case: input has no test _conditions
    if test_condition is None:
      return True

    # Check single string, which is typical case
    if isinstance(test_condition, basestring):
      return test_condition in klass.Conditions

    # Check if test_conditions are a subset of current conditions
    test_set = None
    if isinstance(test_condition, set):
      test_set = test_condition
    elif hasattr(test_condition, '__iter__'):
      test_set = set(test_condition)
    else:
      test_set = set([test_condition])

    return test_set <= klass.Conditions

# ---------------------------------------------------------------------
  @classmethod
  def get_associated_entity_ids(klass, att, as_string=False):
    '''Returns list of all model entity ids associated with this attribute
    '''
    reference_item = att.associations()
    return klass.get_model_entity_ids(reference_item, as_string)

# ---------------------------------------------------------------------
  @classmethod
  def get_model_entity_ids(klass, item, as_string=False):
    '''Returns list of model entity ids represented by input item

    Input item could be ModelEntityItem or ReferenceItem
    '''
    if item is None or (0 == item.numberOfValues()):
      return []

    full_id_list = list()
    for i in range(item.numberOfValues()):
      if item.type() == smtk.attribute.Item.ModelEntityType:
        ref = item.value(i)
        ent = model_entity_ref.entity()
      elif item.type() == smtk.attribute.Item.ReferenceType:
        ent = item.objectValue(i)
      else:
        raise Exception('Unsupported type', item.type())

      ent_id = klass.get_model_entity_id(ent)
      if ent_id is not None:
        full_id_list.append(ent_id)

    full_id_list.sort()
    if as_string:
      string_list = [str(id) for id in full_id_list]
      string_value = ', '.join(string_list)
      return string_value

    # (else)
    return full_id_list

# ---------------------------------------------------------------------
  @classmethod
  def get_model_entity_id(klass, model_entity, property_name='pedigree id'):
    '''Returns id assigned to the input model entity.

    Uses integer property assigned by the application
    '''
    if klass.ModelManager is None:
      return None

    idlist = klass.ModelManager.integerProperty(model_entity.id(), property_name)
    if idlist:
      return idlist[0]
    #(else)
    return None

# ---------------------------------------------------------------------
  @staticmethod
  def write_value(
    out, keyword, value, quote_string=True, as_boolean=False, tab=25):
    '''Writes value to output stream

    '''
    if isinstance(value, list):
      value = ', '.join(map(str, value))
      quote_string = False

    # Use str.format() method to set first column width
    if len(keyword) > tab:
      tab = len(keyword)
    text_formatter = '  {:<%s} = {:}\n' % tab
    if as_boolean:
      value = '.true.' if value else '.false.'
    elif quote_string and isinstance(value, basestring):
      value = '\"%s\"' % value
    line = text_formatter.format(keyword, value)
    out.write(line)
