/*
 * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *  
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *  
 *     * Neither the name of Business Objects nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *  
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * RecordCreationGem.java
 * Creation date: July 27, 2007.
 * By: Jennifer Chen
 */

package org.openquark.gems.client;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.openquark.cal.compiler.CompositionNode;
import org.openquark.cal.compiler.FieldName;
import org.openquark.cal.compiler.LanguageInfo;
import org.openquark.cal.compiler.TypeExpr;
import org.openquark.cal.metadata.ArgumentMetadata;
import org.openquark.cal.services.CALFeatureName;
import org.openquark.util.Pair;
import org.openquark.util.xml.BadXMLDocumentException;
import org.openquark.util.xml.XMLPersistenceHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * An RecordCreatonGem is a Gem which creates a record.
 * @author Jennifer Chen
 */

public class RecordCreationGem extends Gem implements CompositionNode.RecordCreationNode {

    // Used for making a valid ordinal field name
    private static final String DEFAULT_NEW_FIELD_PREFIX = "#";

    private static final int DEFAULT_NUM_FIELDS = 2;

    /** Used to suggest the new field and argument name to be created, needs to be checked for uniqueness */
    private int nextPossibleFieldName = 1;

    /** The current list of record fields in order of addition */
    private List<String> fields = new ArrayList<String>();

    /**
     * Constructor. A default number of fields are created.
     */
    public RecordCreationGem() {
        super(0);
        addNewFields(DEFAULT_NUM_FIELDS);
    }

    /**
     * Add new fields to the end of the fields list with a default name then updates the inputs
     * @param numFields the number of fields to add
     */
    public void addNewFields(int numFields) {

        // Save the state before adding new fields
        Map<String, PartInput> fieldNameToInputMap = getFieldNameToInputMap();

        for (int i = 0; i < numFields; i++) {
            fields.add(getUniqueFieldName(fields));
            nextPossibleFieldName++;
        }

        // Update the gem inputs
        updateInputs(fieldNameToInputMap);

    }

    /**
     * Delete the specified field
     * @param fieldToDelete the field to delete from record
     * @throws IllegalArgumentException
     */
    public void deleteField(String fieldToDelete) {

        // Save the state before deleting
        Map<String, PartInput> fieldNameToInputMap = getFieldNameToInputMap();

        if (fields.remove(fieldToDelete)) {
            updateInputs(fieldNameToInputMap);
        } else {
            throw new IllegalArgumentException("The field to delete does not exist.");
        }
    }

    /**
     * Rename an existing field with a valid new field name which does not already exist. Renaming an field to 
     * its original name (essentially no change) is allowed
     * @param fieldIndex the index of the field to rename
     * @param newName the new field name
     * @throws IllegalArgumentException if newName is null or newName is a duplicate of an existing name other than itself. 
     */
    public void renameRecordField(int fieldIndex, String newName) {

        String oldName = fields.get(fieldIndex);

        if (newName == null) {
            throw new IllegalArgumentException("The field cannot be renamed if the new name is full");
        }

        fields.set(fieldIndex, newName);

        PartInput input = this.getInputPart(fieldIndex);
        String newInputName = getArgumentString(newName);
        // this is the displayed name
        input.setOriginalInputName(newInputName);
        // but setting the argument name is necessary to change the display instantly.
        input.setArgumentName(new ArgumentName(newInputName));

        // Update the metadata as well. For now user-set names will be lost when renaming happens
        // this line: input.getDesignMetadata().setDisplayName(newInputName); does not overwrite the existing metadata.
        ArgumentMetadata metadata = new ArgumentMetadata(CALFeatureName.getArgumentFeatureName(fieldIndex), input.getDesignMetadata().getLocale());
        metadata.setDisplayName(newInputName);
        input.setDesignMetadata(metadata);
        
        if (nameChangeListener != null) {
            nameChangeListener.nameChanged(new NameChangeEvent(this, oldName));
        }

    }

    /**
     * Updates the input according the current list of fields. 
     * The updated inputs will be in the order of their corresponding fields.
     * 
     * @param fieldnameToInputMap the map of field to input before the fields were updated, 
     * used to preserve the correct match up for fields and inputs
     */
    void updateInputs(Map<String, PartInput> fieldnameToInputMap) {

        int nNewInputs = fields.size();
        PartInput[] newInputArray = new PartInput[nNewInputs];

        for (int i = 0; i < nNewInputs; i++) {
            String name = fields.get(i);
            PartInput input = fieldnameToInputMap.get(name);
            if (input != null) {
                // existing input
                input.setInputNum(i);
                input.setOriginalInputName(getArgumentString(name));

            } else {
                // new input
                input = createInputPart(i);
                input.setType(TypeExpr.makeParametricType());

                String argName = getArgumentString(name);
                input.setArgumentName(new ArgumentName(argName));
                input.setOriginalInputName(argName);

                // Needs to set metadata because of locale
                ArgumentMetadata metadata = new ArgumentMetadata(CALFeatureName.getArgumentFeatureName(i), input.getDesignMetadata().getLocale());
                metadata.setDisplayName(argName);
                input.setDesignMetadata(metadata);
            }

            newInputArray[i] = input;
        }

        setInputParts(newInputArray);
    }

    /**
     * Get a list of deletable fields
     * @param tabletop
     * @return fields that can be deleted
     */
    public List<String> getDeletableFields(TableTop tabletop) {

        int nArgs = getNInputs();
        List<String> deletableFields = new ArrayList<String>(nArgs);

        // Get all the inputs that are not connected
        for (int i = 0; i < nArgs; i++) {
            PartInput input = this.getInputPart(i);
            if (!input.isConnected()) {
                deletableFields.add(fields.get(i));
            }
        }

        return deletableFields;
    }

    /**
     * Get a list of renamable field 
     * @param tabletop
     * @return fields that can be renamed
     */
    public List<String> getRenamableFields(TableTop tabletop) {
        // For now return all fields are renamable
        return this.getCopyOfFieldsList();

    }

    /**
     * Generates a string for the argument display
     * @param fieldName the field to generate a string for
     * @return a string representing a field's corresponding input
     */
    private String getArgumentString(String fieldName) {
    
        StringBuilder fieldNameStr = new StringBuilder(fieldName);
        if (fieldName.length() > 0 && fieldName.charAt(0) == '#') {
            fieldNameStr.deleteCharAt(0);
        }
    
        return "fld_" + fieldNameStr;
    
    }

    /**
     * Helper method to find an unique new field name
     * @param hasFields the field names that already exists for this gem
     * @return a new field name
     */
    private String getUniqueFieldName(Collection<String> hasFields) {

        for (int i = nextPossibleFieldName; true; i++) {
            String name = DEFAULT_NEW_FIELD_PREFIX + i;

            if (LanguageInfo.isValidFieldName(name) && !hasFields.contains(name)) {
                return name;
            }
        }
    }

    /**
     * Retrieve the most up-to-date fields and their corresponding PartInputs in order of the fields list
     * @return mapping of field names to PartInputs
     */
    public Map<String, PartInput> getFieldNameToInputMap() {

        Map<String, PartInput> fieldToInputMap = new LinkedHashMap<String, PartInput>();
        int nArgs = getNInputs();

        for (int i = 0; i < nArgs; i++) {
            PartInput input = getInputPart(i);
            fieldToInputMap.put(fields.get(i), input);
        }
        
        return fieldToInputMap;
    }

    /**
     * @return a copy of the fields list
     */
    public List<String> getCopyOfFieldsList() {
        List<String> newFieldsList = new ArrayList<String>(fields.size());
        for (String fieldToCopy : fields) {
            newFieldsList.add(fieldToCopy);
        }
        return newFieldsList;
    }

    /**
     * Sets the fields list for this gem. Replaces all existing fields. 
     * Note: this does not update the gem's inputs
     * @param newFields
     */
    void setFieldNames(List<String> newFields) {
        this.fields.clear();

        fields.addAll(newFields);
    }

    /**
     * Sets the next potential ordinal field name to be created
     * @param num the next possible field name, should be > 0
     */
    void setNextPossibleFieldName(int num) {
        if (num > 0) {
            this.nextPossibleFieldName = num;
        }
    }

    /**
     * Gets the NextPossibleFieldName which suggests the ordinal name for next field created 
     * @return int the integer part of the potential ordinal field name
     */
    int getNextPossibleFieldName() {
        return nextPossibleFieldName;
    }

    /**
     * @param index the index of the field in the list
     * @return the field name at the specified index in the field list
     */
    public FieldName getFieldName(int index) {
        return FieldName.make(fields.get(index));
    }

    /**
     * @param field the field to retrieve index for
     * @return the index of the field in question
     */
    public int getFieldIndex(FieldName field) {
        return fields.indexOf(field.getCalSourceForm());

    }

    /**
     * @return the string representation of the field names
     */
    public String getDisplayName() {

        StringBuilder displayName = new StringBuilder("{ ");

        boolean isFirstField = true;
        for (String field : fields) {
            if (isFirstField) {
                isFirstField = false;
            } else {
                displayName.append(", ");
            }
            displayName.append(field);
        }

        displayName.append(" }");
        return displayName.toString();
    }

    /*
     * Methods supporting XMLPersistable ********************************************
     */

    /**
     * {@inheritDoc}
     */
    @Override
    public void saveXML(Node parentNode, GemContext gemContext) {

        if (fields.isEmpty()) {
            throw new IllegalStateException("RecordCreationGem cannot be saved while fields are empty");
        }

        Document document = (parentNode instanceof Document) ? (Document)parentNode : parentNode.getOwnerDocument();

        // Create the RecordCreationGem element
        Element resultElement = document.createElementNS(GemPersistenceConstants.GEM_NS, GemPersistenceConstants.RECORD_CREATION_GEM_TAG);
        resultElement.setPrefix(GemPersistenceConstants.GEM_NS_PREFIX);
        parentNode.appendChild(resultElement);

        // Add info for the superclass gem.
        super.saveXML(resultElement, gemContext);

        // Now add RecordCreationGem specific info

        // Element for the arguments
        Element argumentsElement = document.createElement(GemPersistenceConstants.ARGUMENTS_TAG);
        resultElement.appendChild(argumentsElement);

        // Get and add the element for each argument
        for (final PartInput input : this.getInputParts()) {
            Element argumentElement = GemCutterPersistenceHelper.inputToArgumentElement(input, document, gemContext);
            argumentsElement.appendChild(argumentElement);
        }

        // Element for the field names
        Element fieldNamesElement = document.createElement(GemPersistenceConstants.RECORD_CREATION_GEM_FIELDS_TAG);
        resultElement.appendChild(fieldNamesElement);

        for (final String name : this.fields) {
            XMLPersistenceHelper.addTextElement(fieldNamesElement, GemPersistenceConstants.RECORD_CREATION_GEM_FIELD_TAG, name);
        }
        resultElement.setAttribute(GemPersistenceConstants.RECORD_CREATION_GEM_NEXT_FIELDNAME_ATTR, String.valueOf(nextPossibleFieldName));
    }

    /**
     * Create a new RecordCreationGem and loads its state from the specified XML element.
     * @param gemElement Element the element representing the structure to deserialize.
     * @param gemContext the context in which the gem is being instantiated.
     * @param loadInfo the argument info for this load session.
     * @return RecordCreationGem
     * @throws BadXMLDocumentException
     */
    public static RecordCreationGem getFromXML(Element gemElement, GemContext gemContext, Argument.LoadInfo loadInfo) throws BadXMLDocumentException {
        RecordCreationGem gem = new RecordCreationGem();
        gem.loadXML(gemElement, gemContext, loadInfo);
        return gem;
    }

    /**
     * Load this object's state.
     * @param gemElement Element the element representing the structure to deserialize.
     * @param gemContext the context in which the gem is being instantiated.
     * @param loadInfo loadInfo for the arguments
     */
    void loadXML(Element gemElement, GemContext gemContext, Argument.LoadInfo loadInfo) throws BadXMLDocumentException {

        XMLPersistenceHelper.checkTag(gemElement, GemPersistenceConstants.RECORD_CREATION_GEM_TAG);
        XMLPersistenceHelper.checkPrefix(gemElement, GemPersistenceConstants.GEM_NS_PREFIX);

        List<Element> childElems = XMLPersistenceHelper.getChildElements(gemElement);
        int nChildElems = childElems.size();

        // Get info for the underlying gem.
        Element superGemElem = (childElems.size() < 1) ? null : (Element)childElems.get(0);
        XMLPersistenceHelper.checkIsElement(superGemElem);
        super.loadXML(superGemElem, gemContext);

        // Figure out the record node

        // Get the arguments
        Element argumentsElement = (nChildElems < 2) ? null : (Element)childElems.get(1);
        XMLPersistenceHelper.checkIsElement(argumentsElement);

        List<Element> argumentsChildElems = XMLPersistenceHelper.getChildElements(argumentsElement);
        int nArgumentsChildElems = argumentsChildElems.size();

        // Get the field names
        Element fieldNamesElement = (nChildElems < 3) ? null : (Element)childElems.get(2);
        XMLPersistenceHelper.checkIsElement(fieldNamesElement);

        List<Element> fieldsChildElems = XMLPersistenceHelper.getChildElements(fieldNamesElement);

        if (fieldsChildElems.size() != nArgumentsChildElems) {
            throw new BadXMLDocumentException(null, "Number of fields and inputs do not correspond.");
        }

        // Erase the default fields
        fields.clear();

        for (int i = 0; i < nArgumentsChildElems; i++) {
            Element inputChildElem = argumentsChildElems.get(i);
            Pair<String, Integer> inputInfoPair = GemCutterPersistenceHelper.argumentElementToInputInfo(inputChildElem);

            // Add the argument info the load info.
            loadInfo.addArgument(this, inputInfoPair.fst(), inputInfoPair.snd(), null);

            Element fieldChildElem = fieldsChildElems.get(i);
            String name = XMLPersistenceHelper.getChildText(fieldChildElem);

            // Update the fields list by appending the name to the end of the list
            if (name != null) {
                fields.add(name);
            }
        }

        try {
            nextPossibleFieldName = XMLPersistenceHelper.getIntegerAttributeWithDefault(gemElement, GemPersistenceConstants.RECORD_CREATION_GEM_NEXT_FIELDNAME_ATTR, nArgumentsChildElems);
        } catch (BadXMLDocumentException e) {
            nextPossibleFieldName = nArgumentsChildElems;
        }
    }

    /**
     * Makes a copy of the specified gem.
     * @return a copy of the current RecordCreationGem
     */
    RecordCreationGem makeCopy() {

        if (fields.isEmpty()) {
            throw new IllegalStateException("Record Creation Gem cannot be copied while fields are empty");
        }

        // create a new gem with a copy of the current fields list
        RecordCreationGem newGem = new RecordCreationGem();
        newGem.fields = this.getCopyOfFieldsList();
        newGem.nextPossibleFieldName = this.nextPossibleFieldName;
        newGem.updateInputs(newGem.getFieldNameToInputMap());

        return newGem;
    }

}
