/*
 * Copyright (c) 2008. All rights reserved.
 */
package ro.isdc.wro.model.factory;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.apache.commons.io.input.AutoCloseInputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.model.WroModel;
import ro.isdc.wro.model.WroModelInspector;
import ro.isdc.wro.model.group.Group;
import ro.isdc.wro.model.group.Inject;
import ro.isdc.wro.model.group.RecursiveGroupDefinitionException;
import ro.isdc.wro.model.group.processor.Injector;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.util.StopWatch;


/**
 * Model factory implementation. Creates a WroModel object, based on an xml. This xml contains the description of all
 * groups.
 * <p/>
 * This class is thread-safe (the create method is synchronized).
 *
 * @author Alex Objelean
 * @created Created on Nov 3, 2008
 */
public class XmlModelFactory
    extends AbstractWroModelFactory {
  /**
   * Logger for this class.
   */
  private static final Logger LOG = LoggerFactory.getLogger(XmlModelFactory.class);
  /**
   * The alias for this model factory used by spi provider.
   */
  public static final String ALIAS = "xml";
  /**
   * Default xml filename.
   */
  private static final String DEFAULT_FILE_NAME = "wro.xml";

  /**
   * Default xml to parse.
   */
  private static final String XML_SCHEMA_FILE = "wro.xsd";

  /**
   * Group tag used in xml.
   */
  private static final String TAG_GROUP = "group";
  /**
   * Import tag used in xml.
   */
  private static final String TAG_IMPORT = "import";
  /**
   * CSS tag used in xml.
   */
  private static final String TAG_CSS = "css";

  /**
   * JS tag used in xml.
   */
  private static final String TAG_JS = "js";

  /**
   * GroupRef tag used in xml.
   */
  private static final String TAG_GROUP_REF = "group-ref";

  /**
   * Group name attribute.
   */
  private static final String ATTR_GROUP_NAME = "name";
  /**
   * Group abstract attribute used to distinguish an abstract group when its value is true. By default the value is
   * false.
   */
  private static final String ATTR_GROUP_ABSTRACT = "abstract";
  /**
   * Minimize attribute specified on resource level, used to turn on/off minimization on this particular resource during
   * pre processing.
   */
  private static final String ATTR_MINIMIZE = "minimize";
  /**
   * Map between the group name and corresponding element. Hold the map<GroupName, Element> of all group nodes to access
   * any element.
   */
  final Map<String, Element> allGroupElements = new HashMap<String, Element>();

  /**
   * List of groups which are currently being processing and are partially parsed. This list is useful in order to catch
   * infinite recurse group reference.
   */
  final Collection<String> groupsInProcess = new HashSet<String>();

  /**
   * Used to locate imports;
   */
  @Inject
  private UriLocatorFactory locatorFactory;
  @Inject
  private Injector injector;
  /**
   * Used to detect recursive import processing.
   */
  private final Set<String> processedImports = new HashSet<String>();
  /**
   * Flag for enabling xml validation.
   */
  private boolean validateXml = true;
  /**
   * The model being created.
   */
  private WroModel model;

  /**
   * Default constructor.
   */
  public XmlModelFactory() {
  }

  /**
   * Allow aggregate processed imports during recursive model creation.
   */
  private XmlModelFactory(final Set<String> processedImports) {
    Validate.notNull(processedImports);
    this.processedImports.addAll(processedImports);
  }

  /**
   * {@inheritDoc}
   */
  public synchronized WroModel create() {
    model = new WroModel();
    final StopWatch stopWatch = new StopWatch("Create Wro Model from XML");
    try {
      stopWatch.start("createDocument");
      final Document document = createDocument();
      stopWatch.stop();

      stopWatch.start("processGroups");
      processGroups(document);
      stopWatch.stop();

      stopWatch.start("processImports");
      processImports(document);
      stopWatch.stop();

      stopWatch.start("createModel");
      parseGroups();
      stopWatch.stop();
      return model;
    } finally {
      // clear the processed imports even when the model creation fails.
      processedImports.clear();
      LOG.debug(stopWatch.prettyPrint());
    }
  }

  /**
   * @return valid {@link Document} of the xml containing model representation.
   */
  private Document createDocument() {
    try {
      final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setNamespaceAware(true);
      final Document document = factory.newDocumentBuilder().parse(getModelResourceAsStream());
      document.getDocumentElement().normalize();
      if (isValidateXml()) {
        validate(document);
      }
      return document;
    } catch (final Exception e) {
      throw new WroRuntimeException("Cannot build model from XML: " + e.getMessage(), e);
    }
  }

  /**
   * @param document
   *          xml document to validate.
   */
  private void validate(final Document document)
      throws IOException, SAXException {
    final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    final Schema schema = factory.newSchema(new StreamSource(getSchemaStream()));
    schema.newValidator().validate(new DOMSource(document));
  }

  private InputStream getSchemaStream()
      throws IOException {
    // use the class located in same package where xsd is located
    return WroRuntimeException.class.getResourceAsStream(XML_SCHEMA_FILE);
  }

  /**
   * Initialize the map
   */
  private void processGroups(final Document document) {
    // handle imports
    final NodeList groupNodeList = document.getElementsByTagName(TAG_GROUP);
    for (int i = 0; i < groupNodeList.getLength(); i++) {
      final Element groupElement = (Element) groupNodeList.item(i);
      final String name = groupElement.getAttribute(ATTR_GROUP_NAME);
      allGroupElements.put(name, groupElement);
    }
  }

  private void processImports(final Document document) {
    final NodeList importsList = document.getElementsByTagName(TAG_IMPORT);
    LOG.debug("number of imports: {}", importsList.getLength());
    for (int i = 0; i < importsList.getLength(); i++) {
      final Element element = (Element) importsList.item(i);
      final String name = element.getTextContent();
      LOG.debug("processing import: {}", name);
      Validate.notNull(locatorFactory, "The Locator cannot be null!");
      if (processedImports.contains(name)) {
        final String message = "Recursive import detected: " + name;
        LOG.error(message);
        throw new RecursiveGroupDefinitionException(message);
      }

      processedImports.add(name);
      model.merge(createImportedModel(name));
    }
  }

  /**
   * @param modelLocation
   *          the path where the model is located.
   * @return the {@link WroModel} created from provided modelLocation.
   */
  private WroModel createImportedModel(final String modelLocation) {
    final XmlModelFactory importedModelFactory = new XmlModelFactory(this.processedImports) {
      @Override
      protected InputStream getModelResourceAsStream()
          throws IOException {
        LOG.debug("build model from import: {}", modelLocation);
        return new AutoCloseInputStream(locatorFactory.locate(modelLocation));
      };
    };
    // inject manually created modelFactory
    injector.inject(importedModelFactory);
    try {
      return importedModelFactory.create();
    } catch (final WroRuntimeException e) {
      LOG.error("Detected invalid model import from location {}", modelLocation);
      throw e;
    }
  }

  /**
   * Parse the document and creates groups which are added to the provided model.
   *
   * @param document
   *          to parse.
   */
  private void parseGroups() {
    // use groups created by imports (if any)
    for (final Element element : allGroupElements.values()) {
      parseGroup(element);
    }
  }

  /**
   * Recursive method. Add the parsed element group to the group collection. If the group contains group-ref element,
   * parse recursively this group.
   *
   * @param element
   *          Group Element to parse.
   * @param groups
   *          list of parsed groups where the parsed group is added..
   * @return list of resources associated with this resource
   */
  private Collection<Resource> parseGroup(final Element element) {
    final String name = element.getAttribute(ATTR_GROUP_NAME);
    final String isAbstractAsString = element.getAttribute(ATTR_GROUP_ABSTRACT);
    final boolean isAbstractGroup = StringUtils.isNotEmpty(isAbstractAsString) && Boolean.valueOf(isAbstractAsString);
    if (groupsInProcess.contains(name)) {
      throw new RecursiveGroupDefinitionException("Infinite Recursion detected for the group: " + name
          + ". Recursion path: " + groupsInProcess);
    }
    LOG.debug("\tadding group: {}", name);
    groupsInProcess.add(name);
    // skip if this group is already parsed
    final Group parsedGroup = new WroModelInspector(model).getGroupByName(name);
    if (parsedGroup != null) {
      // remove before returning
      // this group is parsed, remove from unparsed groups collection
      groupsInProcess.remove(name);
      return parsedGroup.getResources();
    }
    final Group group = new Group(name);
    final List<Resource> resources = new ArrayList<Resource>();
    final NodeList resourceNodeList = element.getChildNodes();
    for (int i = 0; i < resourceNodeList.getLength(); i++) {
      final Node node = resourceNodeList.item(i);
      if (node instanceof Element) {
        final Element resourceElement = (Element) node;
        parseResource(resourceElement, resources);
      }
    }
    group.setResources(resources);
    // this group is parsed, remove from unparsed collection
    groupsInProcess.remove(name);
    if (!isAbstractGroup) {
      // add only non abstract groups
      model.addGroup(group);
    }
    return resources;
  }

  /**
   * Creates a resource from a given resourceElement. It can be css, js. If resource tag name is group-ref, the method
   * will start a recursive computation.
   *
   * @param resourceElement
   * @param resources
   *          list of parsed resources where the parsed resource is added.
   */
  private void parseResource(final Element resourceElement, final Collection<Resource> resources) {
    ResourceType type = null;
    final String tagName = resourceElement.getTagName();
    final String uri = resourceElement.getTextContent();
    if (TAG_JS.equals(tagName)) {
      type = ResourceType.JS;
    } else if (TAG_CSS.equals(tagName)) {
      type = ResourceType.CSS;
    } else if (TAG_GROUP_REF.equals(tagName)) {
      // uri in this case is the group name
      resources.addAll(getResourcesForGroup(uri));
    }
    if (type != null) {
      final String minimizeAsString = resourceElement.getAttribute(ATTR_MINIMIZE);
      final boolean minimize = StringUtils.isEmpty(minimizeAsString) ? true
          : Boolean.valueOf(resourceElement.getAttribute(ATTR_MINIMIZE));
      final Resource resource = Resource.create(uri, type);
      resource.setMinimize(minimize);
      LOG.debug("\t\tadding resource: {}", resource);
      resources.add(resource);
    }
  }

  /**
   * Search for all resources for a group with a given name.
   */
  private Collection<Resource> getResourcesForGroup(final String groupName) {
    final WroModelInspector modelInspector = new WroModelInspector(model);
    final Group foundGroup = modelInspector.getGroupByName(groupName);
    if (foundGroup == null) {
      Collection<Resource> groupResources = null;
      final Element groupElement = allGroupElements.get(groupName);
      if (groupElement == null) {
        throw new WroRuntimeException("Invalid group-ref: " + groupName);
      }
      groupResources = parseGroup(groupElement);
      return groupResources;
    }
    return foundGroup.getResources();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected String getDefaultModelFilename() {
    return DEFAULT_FILE_NAME;
  }

  /**
   * @return true if xml validation should be performed.
   */
  private boolean isValidateXml() {
    return this.validateXml;
  }

  /**
   * Allows disable the xml validation (which is true by default. Be aware that this is not a good idea to disable
   * validation, because you cannot be sure that the model is built correctly. Disabling makes sense only when for some
   * reason the xsd schema cannot be loaded on some environments. An example of issue can be found here:<br/>
   * <a href="http://code.google.com/p/wro4j/issues/detail?id=371">Wro4j doesn't work on Websphere with 2 or more
   * wars</a>
   */
  public XmlModelFactory setValidateXml(final boolean validateXml) {
    this.validateXml = validateXml;
    return this;
  }
}
