/*
 * Copyright (c) 2017 XMLmind Software. All rights reserved.
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind Assembly Processor project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */
package com.xmlmind.assembly;

import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import com.xmlmind.util.ObjectUtil;
import com.xmlmind.util.ArrayUtil;
import com.xmlmind.util.StringUtil;
import com.xmlmind.util.XMLText;
import com.xmlmind.domutil.DOMUtil;

/**
 * A conditional processing profile.
 */
public final class Profile {
    /**
     * A profiling attribute.
     */
    @SuppressWarnings("overrides")
    public static final class AttributeEntry {
        /**
         * The namespace URI of this profiling attribute.
         */
        public final String nsURI;

        /**
         * The local name of this profiling attribute.
         */
        public final String localName;

        /**
         * The "tokens" comprising the value of this profiling attribute.
         */
        public final String[] values;

        /**
         * Constructs an entry.
         */
        public AttributeEntry(String nsURI, String localName, String[] values) {
            this.nsURI = nsURI;
            this.localName = localName;
            this.values = values;
        }

        @Override
        public boolean equals(Object other) {
            if (other == null || !(other instanceof AttributeEntry)) {
                return false;
            }

            AttributeEntry o = (AttributeEntry) other;
            return (ObjectUtil.equals(nsURI, o.nsURI) && 
                    localName.equals(o.localName));
        }
    }

    /**
     * The character used to separate "tokens" when the value 
     * of a profiling attribute has multiple tokens.
     */
    public final char valueSeparator;

    private AttributeEntry[] entries;

    // -----------------------------------------------------------------------

    /**
     * Constructs a profile using specified character as a token separator.
     *
     * @see #valueSeparator
     */
    public Profile(char separ) {
        valueSeparator = separ;

        clearEntries();
    }

    /**
     * Remove all profiling attributes.
     */
    public void clearEntries() {
        entries = new AttributeEntry[0];
    }

    /**
     * Add or replace specified profiling attribute.
     *
     * @param attrName qualified name (using Clark's notation) 
     * of the profiling attribute
     * @param attrValue value, possibly containing multiple "tokens",
     * of the profiling attribute
     * @exception IllegalArgumentException if attribute name cannot be parsed
     * or attribute value is null or empty
     */
    public void putEntry(String attrName, String attrValue) 
        throws IllegalArgumentException {
        String[] parsed = DOMUtil.parseName(attrName);
        if (parsed == null) {
            throw new IllegalArgumentException(
                Msg.msg("invalidProfilingAttributeName", attrName));
        }

        String[] values = splitEffectivityValue(attrValue, valueSeparator);
        if (values == null || values.length == 0) {
            throw new IllegalArgumentException(
                Msg.msg("invalidProfilingAttributeValue", attrValue));
        }

        putEntry(new AttributeEntry(parsed[0], parsed[1], values));
    }

    /**
     * Utility method: split specified profiling attribute value 
     * into multiple "token" using specified separator.
     */
    public static String[] splitEffectivityValue(String value, char separ) {
        String[] values = null;

        if (value != null) {
            value = value.trim();

            if (value.indexOf(separ) >= 0) {
                String[] tokens;
                if (separ == ' ') {
                    tokens = XMLText.splitList(value);
                } else {
                    tokens = StringUtil.split(value, separ);
                }
                final int count = tokens.length;

                values = new String[count];
                int j = 0;

                for (int i = 0; i < count; ++i) {
                    String token = tokens[i].trim();
                    if (token.length() > 0) {
                        // No duplicates.
                        boolean found = false;
                        for (int k = 0; k < j; ++k) {
                            if (token.equals(values[k])) {
                                found = true;
                                break;
                            }
                        }

                        if (!found) {
                            values[j++] = token;
                        }
                    }
                }

                if (j == 0) {
                    values = null;
                } else {
                    if (j != count) {
                        values = ArrayUtil.trimToSize(values, j);
                    }
                }
            } else {
                if (value.length() > 0) {
                    values = new String[] { value };
                }
            }
        }

        return values;
    }

    /**
     * Add or replace specified profiling attribute.
     */
    public void putEntry(AttributeEntry entry) {
        int index = ArrayUtil.indexOf(entries, entry);
        if (index >= 0) {
            entries[index] = entry;
        } else {
            entries = ArrayUtil.append(entries, entry);
        }
    }

    /**
     * Returns an array containing all profiling attributes.
     */
    public AttributeEntry[] getEntries() {
        return entries;
    }

    /**
     * Process specified document.
     */
    public void process(Document doc) {
        process(doc.getDocumentElement());
    }

    private void process(Element tree) {
        // If the element has any of the attributes specified in entries 
        // (profiling attributes defining the selected profile) then 
        // consider it, otherwise ignore it.
        // 
        // If the element has one or more of the profiling attributes NOT
        // having any of the values of the selected profile, this element must
        // be excluded.

        boolean excluded = false;

        boolean profiled = false;
        for (AttributeEntry entry : entries) {
            if (DOMUtil.getNonEmptyAttribute(tree, entry.nsURI,
                                             entry.localName) != null) {
                profiled = true;
                break;
            }
        }

        if (profiled) {
            for (AttributeEntry entry : entries) {
                String value = 
                    DOMUtil.getNonEmptyAttribute(tree, 
                                                 entry.nsURI, entry.localName);
                if (value != null) {
                    String[] tokens = 
                        splitEffectivityValue(value, valueSeparator);
                    if (tokens != null && tokens.length > 0) {
                        boolean exclude = true;

                        for (String token : tokens) {
                            if (ArrayUtil.indexOf(entry.values, token) >= 0) {
                                // This profiling attribute has one of the
                                // values of the selected profile.
                                exclude = false;
                                break;
                            }
                        }

                        if (exclude) {
                            Element parent = DOMUtil.getParentElement(tree);
                            if (parent != null) {
                                // Don't remove the root element.
                                parent.removeChild(tree);
                                excluded = true;
                            }
                            break;
                        }
                    }
                }
            }
        }

        // ---

        if (!excluded) {
            Node child = tree.getFirstChild();
            while (child != null) {
                Node next = child.getNextSibling();

                if (child.getNodeType() == Node.ELEMENT_NODE) {
                    process((Element) child);
                }

                child = next;
            }
        }
    }
}
