/******************************************************************************
 * Copyright (C) 2010-2016 CERN. All rights not expressly granted are reserved.
 *
 * This file is part of the CERN Control and Monitoring Platform 'C2MON'.
 * C2MON is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the license.
 *
 * C2MON 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 Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with C2MON. If not, see <http://www.gnu.org/licenses/>.
 *****************************************************************************/
package cern.c2mon.shared.common.datatag.address.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import cern.c2mon.shared.common.ConfigurationException;
import cern.c2mon.shared.common.datatag.address.HardwareAddress;
import cern.c2mon.shared.common.datatag.address.HardwareAddressFactory;
import cern.c2mon.shared.util.parser.SimpleXMLParser;

/**
 * Implementation of the HardwareAddress interface and of the abstract HardwareAddressFactory class.
 *
 * @author J. Stowisek
 * @version $Revision: 1.4 $ ($Date: 2009/04/02 16:54:50 $ - $State: Exp $)
 */
@Slf4j
public class HardwareAddressImpl extends HardwareAddressFactory implements HardwareAddress {

  /** Serial UID */
  private static final long serialVersionUID = -7336624461787666236L;

  private static SimpleXMLParser xmlParser = null;

  /**
   * Decodes a field name from XML notation (e.g. my-field-name) to a valid Java field name (e.g. myFieldName)
   */
  private static final String decodeFieldName(final String pXmlFieldName) {
    // StringBuilder for constructing the resulting field name
    StringBuilder str = new StringBuilder();
    // Number of characters in the XML-encoded field name
    int fieldNameLength = pXmlFieldName.length();

    char currentChar;
    for (int i = 0; i < fieldNameLength; i++) {
      currentChar = pXmlFieldName.charAt(i);
      if (currentChar == '-') {
        str.append(Character.toUpperCase(pXmlFieldName.charAt(++i)));
      } else {
        str.append(currentChar);
      }
    }
    return str.toString();
  }

  /**
   * Encodes a field name in Java notation (e.g. myFieldName) to an XML field name (e.g. my-field-name).
   */
  private final String encodeFieldName(final String pFieldName) {
    StringBuilder str = new StringBuilder();
    int fieldNameLength = pFieldName.length();

    char currentChar;
    for (int i = 0; i != fieldNameLength; i++) {
      currentChar = pFieldName.charAt(i);
      if (Character.isUpperCase(currentChar)) {
        str.append('-');
        str.append(Character.toLowerCase(currentChar));
      } else {
        str.append(currentChar);
      }
    }
    return str.toString();
  }

  /**
   * Create a HardwareAddress object from its XML representation.
   *
   * @param pElement DOM element containing the XML representation of a HardwareAddress object, as created by the
   *                 toConfigXML() method.
   * @throws RuntimeException if unable to instantiate the Hardware address
   * @see cern.c2mon.shared.common.datatag.address.HardwareAddress#toConfigXML()
   */
  public final synchronized HardwareAddress fromConfigXML(Element pElement) {
    Class hwAddressClass = null;
    HardwareAddressImpl hwAddress = null;

    try {
      hwAddressClass = Class.forName(pElement.getAttribute("class"));
      hwAddress = (HardwareAddressImpl) hwAddressClass.newInstance();
    } catch (ClassNotFoundException cnfe) {
      cnfe.printStackTrace();
      throw new RuntimeException("Exception caught when instantiating a hardware address from XML", cnfe);
    } catch (IllegalAccessException iae) {
      iae.printStackTrace();
      throw new RuntimeException("Exception caught when instantiating a hardware address from XML", iae);
    } catch (InstantiationException ie) {
      ie.printStackTrace();
      throw new RuntimeException("Exception caught when instantiating a hardware address from XML", ie);
    }

    NodeList fields = pElement.getChildNodes();
    Node fieldNode = null;
    int fieldsCount = fields.getLength();
    String fieldName;
    String fieldValueString;
    String fieldTypeName = "";

    for (int i = 0; i < fieldsCount; i++) {
      fieldNode = fields.item(i);
      if (fieldNode.getNodeType() == Node.ELEMENT_NODE) {
        fieldName = fieldNode.getNodeName();

        if (fieldNode.getFirstChild() != null) {
          fieldValueString = fieldNode.getFirstChild().getNodeValue();
        } else {
          fieldValueString = "";
        }
        try {
          Field field = hwAddressClass.getDeclaredField(decodeFieldName(fieldName));
          fieldTypeName = field.getType().getName();

          if (fieldTypeName.equals("short")) {
            field.setShort(hwAddress, Short.parseShort(fieldValueString));
          } else if (fieldTypeName.equals("java.lang.Short")) {
            field.set(hwAddress, new Integer(Integer.parseInt(fieldValueString)));
          } else if (fieldTypeName.equals("int")) {
            field.setInt(hwAddress, Integer.parseInt(fieldValueString));
          } else if (fieldTypeName.equals("java.lang.Integer")) {
            field.set(hwAddress, new Integer(Integer.parseInt(fieldValueString)));
          } else if (fieldTypeName.equals("float")) {
            field.setFloat(hwAddress, Float.parseFloat(fieldValueString));
          } else if (fieldTypeName.equals("java.lang.Float")) {
            field.set(hwAddress, new Float(Float.parseFloat(fieldValueString)));
          } else if (fieldTypeName.equals("double")) {
            field.setDouble(hwAddress, Double.parseDouble(fieldValueString));
          } else if (fieldTypeName.equals("java.lang.Double")) {
            field.set(hwAddress, new Double(Double.parseDouble(fieldValueString)));
          } else if (fieldTypeName.equals("long")) {
            field.setLong(hwAddress, Long.parseLong(fieldValueString));
          } else if (fieldTypeName.equals("java.lang.Long")) {
            field.set(hwAddress, new Long(Long.parseLong(fieldValueString)));
          } else if (fieldTypeName.equals("byte")) {
            field.setByte(hwAddress, Byte.parseByte(fieldValueString));
          } else if (fieldTypeName.equals("java.lang.Byte")) {
            field.set(hwAddress, new Byte(Byte.parseByte(fieldValueString)));
          } else if (fieldTypeName.equals("char")) {
            field.setChar(hwAddress, fieldValueString.charAt(0));
          } else if (fieldTypeName.equals("java.lang.Character")) {
            field.set(hwAddress, new Character(fieldValueString.charAt(0)));
          } else if (fieldTypeName.equals("boolean")) {
            field.setBoolean(hwAddress, Boolean.getBoolean(fieldValueString));
          } else if (fieldTypeName.equals("java.lang.Boolean")) {
            field.set(hwAddress, new Boolean(Boolean.getBoolean(fieldValueString)));
          } else if (fieldTypeName.equals("java.util.HashMap")) {
            field.set(hwAddress, SimpleXMLParser.domNodeToMap(fieldNode));
          } else if (field.getType().isEnum()) {
            Object[] enumConstants = field.getType().getEnumConstants();
            for (Object enumConstant : enumConstants) {
              if (enumConstant.toString().equals(fieldValueString)) {
                field.set(hwAddress, enumConstant);
              }
            }
          } else {
            field.set(hwAddress, fieldValueString);
          }
        } catch (NoSuchFieldException nsfe) {
          String errorMsg = "fromConfigXML(...) - Error occured while parsing XML <HardwareAddress> tag. "
              + "The following variable does not exist in " + hwAddressClass.toString() + ": \""
              + decodeFieldName(fieldName) + "\"";
          log.error(errorMsg);
          throw new IllegalArgumentException(errorMsg);
        } catch (IllegalAccessException iae) {
          iae.printStackTrace();
          throw new RuntimeException(iae);
        } catch (NumberFormatException npe) {
          String errorMsg = "fromConfigXML(...) - Error occured while parsing XML <HardwareAddress> tag. Field \""
              + fieldName + "\" shall not be empty since we expect a \"" + fieldTypeName
              + "\" value. Please correct the XML configuration for " + hwAddressClass.toString();
              log.error(errorMsg);
          throw new IllegalArgumentException(errorMsg);
        }
      }
    }
    return hwAddress;
  }

  public final synchronized HardwareAddress fromConfigXML(final String pXML) {
    try {
      if (xmlParser == null) {
        xmlParser = new SimpleXMLParser();
      }
      return fromConfigXML(xmlParser.parse(pXML).getDocumentElement());
    } catch (Exception e) {
      return null;
    }
  }

  public final synchronized HardwareAddress fromConfigXML(final Document pDocument) {
    return fromConfigXML(pDocument.getDocumentElement());
  }

  /**
   * Returns an XML representation of the HardwareAddress object.
   *
   * @throws RuntimeException if Illegal access to fields
   */
  public final synchronized String toConfigXML() {
    Class handlerClass = this.getClass();
    Field[] fields = handlerClass.getDeclaredFields();

    StringBuilder str = new StringBuilder();

    str.append("        <HardwareAddress class=\"");
    str.append(getClass().getName());
    str.append("\">\n");

    for (int i = 0; i < fields.length; i++) {
      if (Modifier.isProtected(fields[i].getModifiers()) && !Modifier.isFinal(fields[i].getModifiers())) {
        try {
          if (fields[i].get(this) != null) {
            str.append("          <");
            String fieldXMLName = encodeFieldName(fields[i].getName());

            str.append(fieldXMLName);
            str.append(">");
            try {
              str.append(fields[i].get(this));
            } catch (IllegalAccessException iae) {
              iae.printStackTrace();
            }
            str.append("</");
            str.append(fieldXMLName);
            str.append(">\n");
          }
        } catch (IllegalAccessException iae) {
          iae.printStackTrace();
          throw new RuntimeException("Exception caught while converting HardwareAddress to XML.", iae);
        }
      }
    }

    str.append("        </HardwareAddress>\n");
    return str.toString();
  }

  @Override
  public HardwareAddressImpl clone() {
    try {
      return (HardwareAddressImpl) super.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
      throw new RuntimeException("Exception caught while cloning a HardwareAddress", e);
    }
  }

  public final String toConfigXML(HardwareAddress address) {
    return address.toConfigXML();
  }

  public void validate() throws ConfigurationException {

  }

  /**
   * The two addresses are considered equals if they're of the same type and all their non-static attributes are equal
   */
  @Override
  public final boolean equals(final Object copy) {

    boolean result = copy != null && copy instanceof HardwareAddress && this.getClass().equals(copy.getClass());

    if (result) {

      Field[] fields = this.getClass().getDeclaredFields();
      for (Field field : fields) {
        if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())
            && !Modifier.isTransient(field.getModifiers())) {
          try {

            if ((field.get(this) != null && field.get(copy) == null)
                || (field.get(this) == null && field.get(copy) != null)) {
              result = false;
            } else if (field.get(this) != null && field.get(copy) != null) {

              if (field.getType().isArray()) {

                if (Object[].class.isAssignableFrom(field.getType())) {
                  result = Arrays.equals((Object[]) field.get(this), (Object[]) field.get(copy));
                } else {
                  result = ArrayUtils.isEquals(field.get(this), field.get(copy));
                }

              } else {
                result = field.get(this).equals(field.get(copy));
              }
            }
          } catch (Exception e) {
            result = false;
          }
        }

        if (!result) {
          break;
        }
      }
    }

    return result;
  }

  @Override
  public final int hashCode() {

    int result = 0;

    Field[] fields = this.getClass().getDeclaredFields();

    for (Field field : fields) {
      // compare non-final, non-static and non-transient fields only
      if (!Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())
          && !Modifier.isTransient(field.getModifiers())) {
        try {

          // skip arrays
          if (!field.getType().isArray() && field.get(this) != null) {
            // for string take its length
            if (field.getType().equals(String.class)) {
              result ^= ((String) field.get(this)).length();
            } else if (field.getType().equals(short.class) || field.getType().equals(Short.class)) {
              result ^= field.getShort(this);
            } else if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
              result ^= field.getInt(this);
            } else if (field.getType().equals(float.class) || field.getType().equals(Float.class)) {
              result ^= (int) field.getFloat(this);
            } else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
              result ^= (int) field.getDouble(this);
            } else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
              result ^= (int) field.getLong(this);
            } else if (field.getType().equals(byte.class) || field.getType().equals(Byte.class)) {
              result ^= field.getByte(this);
            } else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
              result ^= field.getBoolean(this) == Boolean.TRUE ? 1 : 0;
            }
          }
        } catch (Exception e) {
          log.error(e.toString());
          throw new RuntimeException("Exception caught while calculating HardwareAddress hashcode.", e);
        }
      }
    }
    return result;
  }

  @Override
  public String toString() {
    return toConfigXML();
  }
}