/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2008 jOpenDocument, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU
 * General Public License Version 3 only ("GPL").  
 * You may not use this file except in compliance with the License. 
 * You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html
 * See the License for the specific language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 * 
 */

package org.jopendocument.dom.template.statements;

import org.jopendocument.dom.template.TemplateException;
import org.jopendocument.dom.template.engine.DataModel;
import org.jopendocument.dom.template.engine.Processor;
import org.jopendocument.util.ExceptionUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.logging.Logger;

import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.xpath.XPath;

/**
 * ForEach tag: repeats a document element for each item in a collection. <br>
 * Attributes:
 * <ul>
 * <li><b>element </b> (required): the document element to which this tag applies, e.g. "table-row"
 * </li>
 * <li><b>items </b> (required): expression evaluating to a Collection to be iterated</li>
 * <li><b>var </b> (required): variable name where the current iteration item is stored</li>
 * <li><b>alternate </b> (optional): number of document elements to be alternated during iteration,
 * tipically used for alternating row background colors with a value of "2"</li>
 * </ul>
 */
public class ForEach extends BaseStatement {
    private static Logger logger = Logger.getLogger(ForEach.class.getName());

    public ForEach() {
        super("forEach");
    }

    @SuppressWarnings("unchecked")
    public void prepare(Element script, Element command) throws TemplateException {
        String elementName = command.getAttributeValue("element");
        if (elementName == null)
            throw new TemplateException("missing required attribute for forEach tag: element");
        String itemsExpression = command.getAttributeValue("items");
        if (itemsExpression == null)
            throw new TemplateException("missing required attribute for forEach tag: items");
        String varName = command.getAttributeValue("var");
        if (varName == null)
            throw new TemplateException("missing required attribute for forEach tag: var");
        // alternate
        final int alternate;
        String alternateString = command.getAttributeValue("alternate");
        if (alternateString == null) {
            alternate = 1;
        } else {
            try {
                alternate = Integer.parseInt(alternateString);
            } catch (Throwable t) {
                throw new TemplateException("invalid alternate attribute for forEach tag: " + alternateString);
            }
        }
        // multiple
        final int multiple;
        final String multipleString = command.getAttributeValue("multiple");
        if (multipleString == null) {
            multiple = 0;
        } else {
            try {
                multiple = Integer.parseInt(multipleString);
            } catch (Throwable t) {
                throw new TemplateException("invalid alternate attribute for forEach tag: " + multipleString);
            }
        }
        if (alternate > 1 && multiple > 0) {
            throw new TemplateException("both alternate and multiple have been specified");
        }
        Element element = getAncestorByName(script, elementName);
        if (element == null) {
            throw new TemplateException("no such element enclosing forEach: " + elementName + " for expression: " + itemsExpression);
        }
        Element parent = element.getParentElement();
        // Element forEachTag = parent.addElement(getQName("forEach")).addAttribute("items",
        // itemsExpression).addAttribute("var", varName);
        final Element forEachTag = getElement("forEach").setAttribute("items", itemsExpression).setAttribute("var", varName);
        final int siblingsToAdd;
        if (multiple > 0) {
            forEachTag.setAttribute("multiple", "yes");
            siblingsToAdd = multiple - 1;
        } else {
            siblingsToAdd = alternate - 1;
        }
        int index = parent.indexOf(element);
        List<Element> targets = new ArrayList<Element>();
        targets.add(element);
        if (siblingsToAdd > 0) {
            List siblings;
            try {
                final XPath xpath = XPath.newInstance("following-sibling::*");
                siblings = xpath.selectNodes(element);
            } catch (JDOMException e) {
                throw ExceptionUtils.createExn(TemplateException.class, "xpath error", e);
            }
            if (siblingsToAdd > siblings.size())
                throw new TemplateException("alternate or multiple is greater (" + (siblingsToAdd + 1) + ") than actual table size (" + siblings.size() + ").");
            for (int i = 0; i < siblingsToAdd; i++) {
                Element sibling = (Element) siblings.get(i);
                targets.add(sibling);
                sibling.detach();
            }
        }
        element.detach();
        parent.getContent().add(index, forEachTag);
        for (int i = 0; i < targets.size(); i++) {
            forEachTag.addContent(targets.get(i));
        }

        script.detach();
    }

    @SuppressWarnings("unchecked")
    public void execute(Processor processor, Element tag, DataModel model) throws TemplateException {
        String varName = tag.getAttributeValue("var");
        String itemsExpression = tag.getAttributeValue("items");
        final boolean multiple = "yes".equals(tag.getAttributeValue("multiple"));
        Object value = model.eval(itemsExpression);
        if (value == null) {
            logger.info("forEach items: null expression: " + itemsExpression);
            return;
        }
        final Collection items;
        if (value instanceof Collection)
            items = (Collection) value;
        else if (value instanceof Object[])
            items = Arrays.asList((Object[]) value);
        else
            items = null;
        if (items == null && !(value instanceof Iterator))
            throw new TemplateException("forEach items neither a Collection nor an Iterator: " + itemsExpression + " => " + value.getClass() + ":" + value);
        List targets = tag.getChildren();
        List<Element> parentContent = tag.getParentElement().getContent();
        int iterationCount = 0;
        int index = parentContent.indexOf(tag);
        // ne peut utiliser de Map si plusieurs fois le même éléments dans 'items'
        final List<Object> itemsList = items == null ? new ArrayList<Object>() : new ArrayList<Object>(items.size());
        final List<List<Element>> createdElems = items == null ? new ArrayList<List<Element>>() : new ArrayList<List<Element>>(items.size());
        final Iterator itemsIter = (Iterator) (items != null ? items.iterator() : value);
        // first we create all elements
        while (itemsIter.hasNext()) {
            final Object item = itemsIter.next();
            final List toAdd;
            if (multiple) {
                toAdd = targets;
            } else {
                toAdd = Collections.singletonList(targets.get(iterationCount % targets.size()));
                ++iterationCount;
            }
            final List<Element> added = new ArrayList<Element>(toAdd.size());
            final Iterator targetIter = toAdd.iterator();
            while (targetIter.hasNext()) {
                final Element target = (Element) targetIter.next();
                Element newElement = (Element) target.clone();
                parentContent.add(index++, newElement);
                added.add(newElement);
            }
            // store the new elements and their associated item
            itemsList.add(item);
            createdElems.add(added);
        }
        // then transform them
        // forced to do that otherwise if we transform right after adding, the newly added element
        // might be removed and this will mess with index++
        final ListIterator<Object> i = itemsList.listIterator();
        while (i.hasNext()) {
            final Object item = i.next();
            model.put(varName, item);
            final Iterator addedIter = createdElems.get(i.previousIndex()).iterator();
            while (addedIter.hasNext()) {
                final Element newElement = (Element) addedIter.next();
                processor.transform(newElement);
                removeSection(newElement);
            }
        }

        tag.detach();
    }

}