/*
 * Copyright (c) 2009, Dennis M. Sosnoski. 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
 * JiBX 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.
 */
package org.dataone.jibx.schema.codegen.extend;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.jibx.binding.model.ElementBase;
import org.jibx.schema.codegen.IClassHolder;
import org.jibx.schema.codegen.NameUtils;
import org.jibx.schema.codegen.extend.ClassDecorator;
import org.jibx.schema.codegen.extend.NameMatchDecoratorBase;
import org.jibx.util.NameUtilities;

/**
 * Code generation decorator which modifies the setValue
 * method to check for a maximum length of 800 characters
 * and a nonempty value that contains no whitespace
 *
 * Supplying a 'fields' attribute to the class-decorator element 
 * with space delimited names of the class level fields to apply
 * restrictions to
 * 
 * This Decorator is mutually exclusive with NonEmptyTypeDecorator 
 * when applied to a generated class
 *
 * @author waltz
 */
public class NonEmptyNoWhitespaceString800TypeDecorator extends FieldMatchDecoratorBase implements ClassDecorator {

    /**
     * Parser instance used by class.
     */
    private final ASTParser m_parser = ASTParser.newParser(AST.JLS3);
    // easy way to generate a couple of fields that can be added to the class
    
    private static final String s_classText = "class Gorph { \n"
            + "private final String NONWHITESPACE_REGEX = \"^\\\\S+$\";\n"
            + "Pattern pattern = Pattern.compile(NONWHITESPACE_REGEX);\n}";
    // where $0 is the description text, $1 is the field name, $2 is the value name with initial uppercase character,
    //  $3 is the type, and $4 is a cast if an untyped list is used, or empty if a typed list is used

    /**
     *
     * Default constructor to initialize m_fields with the default String field 'value' that is used in Dataone. Note if
     * this were to be generalized, then 'string' would also be added.
     *
     */
    public NonEmptyNoWhitespaceString800TypeDecorator() {
        super();
    }

    /**
     * Method called after completing code generation for the target class.
     *
     * @param binding
     * @param holder
     */
    public void finish(ElementBase binding, IClassHolder holder) {

        if (matchName(holder.getName())) {

            MethodDeclaration[] methodDeclaration = holder.getMethods();
    
            for (int i = 0; i < methodDeclaration.length; ++i) {
                if (methodDeclaration[i].isConstructor()) {
                    // find constructor methods that use setValue and add a throws clause
                    if (methodDeclaration[i].parameters().size() > 0) {
                        AST ast = methodDeclaration[i].getAST();
                        methodDeclaration[i].thrownExceptions().add(ast.newSimpleName("UnsupportedType"));
                    }
                } 
            }
        }
    }

    /**
     * Method called before starting code generation for the target class.
     *
     * @param holder
     */
    public void start(IClassHolder holder) {

        if (matchName(holder.getName())) {
            holder.addImport("java.util.regex.Pattern");
            holder.addImport("java.util.regex.Matcher");
            holder.addImport("org.dataone.service.exceptions.UnsupportedType");
            // parse the ast source text into a compilation unit 
            // such that the initial fields may be extracted and 
            // and added to the class level fields
            StringBuilder buff = new StringBuilder(s_classText);
            m_parser.setSource(buff.toString().toCharArray());
            CompilationUnit unit = (CompilationUnit) m_parser.createAST(null);

            // add all methods from output tree to class under construction
            TypeDeclaration typedecl = (TypeDeclaration) unit.types().get(0);
            for (Iterator iter = typedecl.bodyDeclarations().iterator(); iter.hasNext();) {
                ASTNode node = (ASTNode) iter.next();
                // add in the fields that were generated from the s_classText
                if (node instanceof FieldDeclaration) {
                    holder.addField((FieldDeclaration) node);
                }

            }
        }

    }

    /**
     * Method called after adding each data value to class.
     *
     * @param basename base name used for data value
     * @param collect repeated value flag
     * @param type value type (item value type, in the case of a repeated value)
     * @param field actual field
     * @param getmeth read access method (<code>null</code> if a flag value)
     * @param setmeth write access method (<code>null</code> if a flag value)
     * @param descript value description text
     * @param holder
     */
    public void valueAdded(String basename, boolean collect, String type, FieldDeclaration field,
            MethodDeclaration getmeth, MethodDeclaration setmeth, String descript, IClassHolder holder) {


        if (!collect && matchName(holder.getName())  && m_fields.contains(basename)) {
            // transform the setValue method

            Block valueBlockBody = setmeth.getBody();

            AST ast = valueBlockBody.getAST();

            setmeth.thrownExceptions().add(ast.newSimpleName("UnsupportedType"));

            Block newMethodBlockBody = ast.newBlock();

             /*
             * Matcher nonWhitespaceMatcher = pattern.matcher(value);
             if (nonWhitespaceMatcher.find() && (value.length <= 800)) {
             this.value = value;
             } else {
             throw new UnsupportedType("000", "some exception");
             }
             */

            // Matcher nonWhitespaceMatcher = pattern.matcher(value);
            VariableDeclarationFragment nonWhitespaceMatcherFragment = ast.newVariableDeclarationFragment();
            nonWhitespaceMatcherFragment.setName(ast.newSimpleName("nonWhitespaceMatcher"));
            VariableDeclarationStatement nonWhitespaceMatcherDeclarationStatement = ast.newVariableDeclarationStatement(nonWhitespaceMatcherFragment);
            nonWhitespaceMatcherDeclarationStatement.setType(ast.newSimpleType(ast.newSimpleName("Matcher")));
            newMethodBlockBody.statements().add(nonWhitespaceMatcherDeclarationStatement);

            MethodInvocation patternMatcherMethodInvocation = ast.newMethodInvocation();
            patternMatcherMethodInvocation.setExpression(ast.newSimpleName("pattern"));
            patternMatcherMethodInvocation.setName(ast.newSimpleName("matcher"));
            patternMatcherMethodInvocation.arguments().add(ast.newSimpleName(basename));
            nonWhitespaceMatcherFragment.setInitializer(patternMatcherMethodInvocation);


            //if (nonWhitespaceMatcher.find() && (value.length() <= 800)) {
            IfStatement ifStatement = ast.newIfStatement();
            Block thenBlock = ast.newBlock();
            ifStatement.setThenStatement(thenBlock);
            InfixExpression ifValueExpression = ast.newInfixExpression();
            ifValueExpression.setOperator(InfixExpression.Operator.CONDITIONAL_AND);

            //nonWhitespaceMatcher.find()
            MethodInvocation findPatternMethodMatch = ast.newMethodInvocation();

            findPatternMethodMatch.setExpression(ast.newSimpleName("nonWhitespaceMatcher"));
            findPatternMethodMatch.setName(ast.newSimpleName("find"));

            //value.length <= 800
            InfixExpression valueLengthExpression = ast.newInfixExpression();
            valueLengthExpression.setOperator(InfixExpression.Operator.LESS_EQUALS);
            MethodInvocation valueLengthMethodInvocation = ast.newMethodInvocation();
            valueLengthMethodInvocation.setExpression(ast.newSimpleName(basename));
            valueLengthMethodInvocation.setName(ast.newSimpleName("length"));
            valueLengthExpression.setLeftOperand(valueLengthMethodInvocation);
            NumberLiteral maxSizeLength = ast.newNumberLiteral();
            maxSizeLength.setToken("800");
            valueLengthExpression.setRightOperand(maxSizeLength);

            ifValueExpression.setLeftOperand(findPatternMethodMatch);
            ifValueExpression.setRightOperand(valueLengthExpression);
            ifStatement.setExpression(ifValueExpression);
            newMethodBlockBody.statements().add(ifStatement);

            // this.value = value;
            Assignment valueAssignment = ast.newAssignment();
            valueAssignment.setOperator(Assignment.Operator.ASSIGN);
            FieldAccess valueField = ast.newFieldAccess();
            valueField.setExpression(ast.newThisExpression());
            valueField.setName(ast.newSimpleName(basename));
            valueAssignment.setLeftHandSide(valueField);
            valueAssignment.setRightHandSide(ast.newSimpleName(basename));
            thenBlock.statements().add(ast.newExpressionStatement(valueAssignment));

            Block elseBlock = ast.newBlock();
            ifStatement.setElseStatement(elseBlock);
            ThrowStatement throwUnsupportedType = ast.newThrowStatement();
            ClassInstanceCreation unsupportedTypeClass = ast.newClassInstanceCreation();
            throwUnsupportedType.setExpression(unsupportedTypeClass);
            SimpleType unsupportedType = ast.newSimpleType(ast.newSimpleName("UnsupportedType"));
            elseBlock.statements().add(throwUnsupportedType);

            unsupportedTypeClass.setType(unsupportedType);
            StringLiteral detailCodeLiteral = ast.newStringLiteral();
            // there will be no way of knowing the detail code since
            // we don't know from which rest method it will be called in
            detailCodeLiteral.setLiteralValue("000");

            InfixExpression exceptionMessageExpression = ast.newInfixExpression();
            exceptionMessageExpression.setOperator(InfixExpression.Operator.PLUS);
            exceptionMessageExpression.setLeftOperand(ast.newSimpleName(basename));
            StringLiteral nonConformanceliteral = ast.newStringLiteral();
            nonConformanceliteral.setLiteralValue(" does not conform to specification of " + holder.getFullName());
            exceptionMessageExpression.setRightOperand(nonConformanceliteral);
            unsupportedTypeClass.arguments().add(detailCodeLiteral);
            unsupportedTypeClass.arguments().add(exceptionMessageExpression);

            // delete the old method body and set the new method body
            valueBlockBody.delete();
            setmeth.setBody(newMethodBlockBody);

        } 
    }
}