/*
 * 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.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ForStatement;
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.PrefixExpression;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
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.VariableDeclarationExpression;
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 if a generated class to check for nonempty value
 *
 * 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 NonEmptyNoWhitespaceString800TypeDecorator when applied to a generated
 * class
 *
 * @author waltz
 */
public class NonEmptyStringTypeDecorator extends FieldMatchDecoratorBase implements ClassDecorator {

    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 NONEMPTY_REGEX = \"^[\\\\s]*[\\\\S][\\\\S+\\\\s+]*$\";\n"
            + "Pattern pattern = Pattern.compile(NONEMPTY_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
    private static final String s_methodBlockText = "class Gorph { \n"
            + "public newMethod() {"
            + "for (String value: list) {\n"
            + "Matcher nonWhitespaceMatcher = pattern.matcher(value);\n"
            + "if (! nonWhitespaceMatcher.find()) {\n"
            + "throw new UnsupportedType(\\\"000\\\", \\\" does not conform to specification of \\\" + holder.getFullName());\n"
            + "}\n"
            + "}\n"
            + "}\n";

    /**
     *
     * Default constructor to initialize must call super to include default fields
     *
     */
    public NonEmptyStringTypeDecorator() {
        super();
    }

    /**
     * Replace all occurrences of one string with another in a buffer.
     *
     * @param match
     * @param replace
     * @param buff
     */
    private static void replace(String match, String replace, StringBuffer buff) {
        int base = 0;
        while ((base = buff.indexOf(match, base)) >= 0) {
            buff.replace(base, base + match.length(), replace);
        }
    }

    /**
     * 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 setmeth  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()) {
             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()) {
            IfStatement ifStatement = ast.newIfStatement();
            Block thenBlock = ast.newBlock();
            ifStatement.setThenStatement(thenBlock);

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

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

            ifStatement.setExpression(findPatternMethodMatch);
            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);
        } else if (collect && matchName(holder.getName()) && m_fields.contains(basename)) {
            Block currentBlockBody = setmeth.getBody();

            AST ast = currentBlockBody.getAST();
            setmeth.thrownExceptions().add(ast.newSimpleName("UnsupportedType"));
            String listParamName = "list";
            FieldDeclaration[] fieldDeclarationArray = holder.getFields();
            List<SingleVariableDeclaration> setMethodParameters= setmeth.parameters();
            for (SingleVariableDeclaration setMethodParam: setMethodParameters) {
                if (setMethodParam.getType().isArrayType()) {
                    listParamName = setMethodParam.getName().getFullyQualifiedName();
                }
            }
            
            Block newBody = ast.newBlock();
            /*
            for (int i = 0; i < list.size(); ++i) {
                     Matcher nonWhitespaceMatcher = pattern.matcher(list.get[i]);
                     if (!nonWhitespaceMatcher.find()) {
                            throw new UnsupportedType("000", "some exception");
                     }
            }
            */

            // int i = 0
            VariableDeclarationFragment forInitializer= ast.newVariableDeclarationFragment();
            forInitializer.setName(ast.newSimpleName("i"));
            VariableDeclarationExpression intIExpression = ast.newVariableDeclarationExpression(forInitializer);

            intIExpression.setType(ast.newPrimitiveType(PrimitiveType.INT));
            forInitializer.setInitializer(ast.newNumberLiteral("1"));

            // i < list.size()
            InfixExpression forValueLengthExpression = ast.newInfixExpression();
            forValueLengthExpression.setOperator(InfixExpression.Operator.LESS);
            forValueLengthExpression.setLeftOperand(ast.newSimpleName("i"));
            MethodInvocation listSizeMethodInvocation = ast.newMethodInvocation();
            listSizeMethodInvocation.setExpression(ast.newSimpleName("list"));
            listSizeMethodInvocation.setName(ast.newSimpleName("size"));
            forValueLengthExpression.setRightOperand(listSizeMethodInvocation);

            // ++i
            PrefixExpression forUnaryIncrementExpression = ast.newPrefixExpression();
            forUnaryIncrementExpression.setOperator(PrefixExpression.Operator.INCREMENT);
            forUnaryIncrementExpression.setOperand(ast.newSimpleName("i"));

            ForStatement forExpressionStatement = ast.newForStatement();
            newBody.statements().add(forExpressionStatement);
            forExpressionStatement.initializers().add(intIExpression);
            forExpressionStatement.setExpression(forValueLengthExpression);
            forExpressionStatement.updaters().add(forUnaryIncrementExpression);

            Block forStatementBody = ast.newBlock();
            forExpressionStatement.setBody(forStatementBody);
            // 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")));
            forStatementBody.statements().add(nonWhitespaceMatcherDeclarationStatement);

            MethodInvocation patternMatcherMethodInvocation = ast.newMethodInvocation();
            patternMatcherMethodInvocation.setExpression(ast.newSimpleName("pattern"));
            patternMatcherMethodInvocation.setName(ast.newSimpleName("matcher"));
            MethodInvocation listGetMethodInvocation = ast.newMethodInvocation();
            listGetMethodInvocation.setExpression(ast.newSimpleName(listParamName));
            listGetMethodInvocation.setName(ast.newSimpleName("get")); 
            listGetMethodInvocation.arguments().add(ast.newSimpleName("i"));      
            patternMatcherMethodInvocation.arguments().add(listGetMethodInvocation);
            
 
            
            nonWhitespaceMatcherFragment.setInitializer(patternMatcherMethodInvocation);

            // if (!nonWhitespaceMatcher.find()) {

            IfStatement ifStatement = ast.newIfStatement();
            Block thenBlock = ast.newBlock();
            ifStatement.setThenStatement(thenBlock);
            PrefixExpression notNonEmptyFindExpression = ast.newPrefixExpression();
            notNonEmptyFindExpression.setOperator(PrefixExpression.Operator.NOT);
            ifStatement.setExpression(notNonEmptyFindExpression);


            MethodInvocation findPatternMethodMatch = ast.newMethodInvocation();

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

            forStatementBody.statements().add(ifStatement);
            // throw new UnsupportedType("000", "some exception");
            ThrowStatement throwUnsupportedType = ast.newThrowStatement();
            ClassInstanceCreation unsupportedTypeClass = ast.newClassInstanceCreation();
            throwUnsupportedType.setExpression(unsupportedTypeClass);
            SimpleType unsupportedType = ast.newSimpleType(ast.newSimpleName("UnsupportedType"));
            thenBlock.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);
            StringLiteral nonConformanceliteral = ast.newStringLiteral();
            nonConformanceliteral.setLiteralValue(basename + " does not conform to specification of " + holder.getFullName());
         //   exceptionMessageExpression.setRightOperand(nonConformanceliteral);
            unsupportedTypeClass.arguments().add(detailCodeLiteral);
            unsupportedTypeClass.arguments().add(nonConformanceliteral);

                        // 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(listParamName));
            newBody.statements().add(ast.newExpressionStatement(valueAssignment));
            
            setmeth.getBody().delete();
            setmeth.setBody(newBody);
        }
    }
}