
package org.mule.modules.quickbooks.config;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.mule.DefaultMuleEvent;
import org.mule.DefaultMuleMessage;
import org.mule.api.MessagingException;
import org.mule.api.MuleContext;
import org.mule.api.MuleEvent;
import org.mule.api.MuleException;
import org.mule.api.MuleMessage;
import org.mule.api.construct.FlowConstruct;
import org.mule.api.construct.FlowConstructAware;
import org.mule.api.context.MuleContextAware;
import org.mule.api.expression.ExpressionManager;
import org.mule.api.lifecycle.Disposable;
import org.mule.api.lifecycle.Initialisable;
import org.mule.api.lifecycle.InitialisationException;
import org.mule.api.lifecycle.Startable;
import org.mule.api.lifecycle.Stoppable;
import org.mule.api.processor.MessageProcessor;
import org.mule.api.registry.RegistrationException;
import org.mule.api.transformer.DataType;
import org.mule.api.transformer.Transformer;
import org.mule.api.transformer.TransformerException;
import org.mule.config.i18n.CoreMessages;
import org.mule.config.i18n.MessageFactory;
import org.mule.modules.quickbooks.QuickBooksModule;
import org.mule.transformer.TransformerTemplate;
import org.mule.transformer.types.DataTypeFactory;
import org.mule.transport.NullPayload;
import org.mule.util.TemplateParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * UpdateVendorMessageProcessor invokes the {@link org.mule.modules.quickbooks.QuickBooksModule#updateVendor(java.lang.String, java.lang.String, java.util.Map, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.List, java.lang.Integer, java.lang.String, java.lang.Boolean, java.util.List, java.util.List, java.util.List)} method in {@link QuickBooksModule }. For each argument there is a field in this processor to match it.  Before invoking the actual method the processor will evaluate and transform where possible to the expected argument type.
 * 
 */
public class UpdateVendorMessageProcessor
    implements FlowConstructAware, MuleContextAware, Disposable, Initialisable, Startable, Stoppable, MessageProcessor
{

    private Object accessToken;
    private String _accessTokenType;
    private Object accessTokenSecret;
    private String _accessTokenSecretType;
    private Object id;
    private Map<String, Object> _idType;
    private Object syncToken;
    private String _syncTokenType;
    private Object name;
    private String _nameType;
    private Object givenName;
    private String _givenNameType;
    private Object middleName;
    private String _middleNameType;
    private Object familyName;
    private String _familyNameType;
    private Object dBAName;
    private String _dBANameType;
    private Object showAs;
    private String _showAsType;
    private Object webSite;
    private List<Map<String, Object>> _webSiteType;
    private Object taxIdentifier;
    private Integer _taxIdentifierType;
    private Object acctNum;
    private String _acctNumType;
    private Object vendor1099;
    private Boolean _vendor1099Type;
    private Object email;
    private List<String> _emailType;
    private Object phone;
    private List<Map<String, Object>> _phoneType;
    private Object address;
    private List<Map<String, Object>> _addressType;
    private static Logger logger = LoggerFactory.getLogger(UpdateVendorMessageProcessor.class);
    /**
     * Module object
     * 
     */
    private Object moduleObject;
    /**
     * Mule Context
     * 
     */
    private MuleContext muleContext;
    /**
     * Mule Expression Manager
     * 
     */
    private ExpressionManager expressionManager;
    /**
     * Mule Pattern Info
     * 
     */
    private TemplateParser.PatternInfo patternInfo;
    /**
     * Flow construct
     * 
     */
    private FlowConstruct flowConstruct;
    /**
     * Variable used to track how many retries we have attempted on this message processor
     * 
     */
    private AtomicInteger retryCount;
    /**
     * Maximum number of retries that can be attempted.
     * 
     */
    private int retryMax;

    /**
     * Obtains the expression manager from the Mule context and initialises the connector. If a target object  has not been set already it will search the Mule registry for a default one.
     * 
     * @throws InitialisationException
     */
    public void initialise()
        throws InitialisationException
    {
        retryCount = new AtomicInteger();
        expressionManager = muleContext.getExpressionManager();
        patternInfo = TemplateParser.createMuleStyleParser().getStyle();
        if (moduleObject == null) {
            try {
                moduleObject = muleContext.getRegistry().lookupObject(QuickBooksModuleOAuth1Adapter.class);
                if (moduleObject == null) {
                    throw new InitialisationException(MessageFactory.createStaticMessage("Cannot find object"), this);
                }
            } catch (RegistrationException e) {
                throw new InitialisationException(CoreMessages.initialisationFailure("org.mule.modules.quickbooks.config.QuickBooksModuleOAuth1Adapter"), e, this);
            }
        }
    }

    public void start()
        throws MuleException
    {
    }

    public void stop()
        throws MuleException
    {
    }

    public void dispose() {
    }

    /**
     * Set the Mule context
     * 
     * @param context Mule context to set
     */
    public void setMuleContext(MuleContext context) {
        this.muleContext = context;
    }

    /**
     * Sets flow construct
     * 
     * @param flowConstruct Flow construct to set
     */
    public void setFlowConstruct(FlowConstruct flowConstruct) {
        this.flowConstruct = flowConstruct;
    }

    /**
     * Sets the instance of the object under which the processor will execute
     * 
     * @param moduleObject Instace of the module
     */
    public void setModuleObject(Object moduleObject) {
        this.moduleObject = moduleObject;
    }

    /**
     * Sets retryMax
     * 
     * @param value Value to set
     */
    public void setRetryMax(int value) {
        this.retryMax = value;
    }

    /**
     * Sets middleName
     * 
     * @param value Value to set
     */
    public void setMiddleName(Object value) {
        this.middleName = value;
    }

    /**
     * Sets taxIdentifier
     * 
     * @param value Value to set
     */
    public void setTaxIdentifier(Object value) {
        this.taxIdentifier = value;
    }

    /**
     * Sets phone
     * 
     * @param value Value to set
     */
    public void setPhone(Object value) {
        this.phone = value;
    }

    /**
     * Sets dBAName
     * 
     * @param value Value to set
     */
    public void setDBAName(Object value) {
        this.dBAName = value;
    }

    /**
     * Sets accessToken
     * 
     * @param value Value to set
     */
    public void setAccessToken(Object value) {
        this.accessToken = value;
    }

    /**
     * Sets accessTokenSecret
     * 
     * @param value Value to set
     */
    public void setAccessTokenSecret(Object value) {
        this.accessTokenSecret = value;
    }

    /**
     * Sets acctNum
     * 
     * @param value Value to set
     */
    public void setAcctNum(Object value) {
        this.acctNum = value;
    }

    /**
     * Sets familyName
     * 
     * @param value Value to set
     */
    public void setFamilyName(Object value) {
        this.familyName = value;
    }

    /**
     * Sets syncToken
     * 
     * @param value Value to set
     */
    public void setSyncToken(Object value) {
        this.syncToken = value;
    }

    /**
     * Sets givenName
     * 
     * @param value Value to set
     */
    public void setGivenName(Object value) {
        this.givenName = value;
    }

    /**
     * Sets vendor1099
     * 
     * @param value Value to set
     */
    public void setVendor1099(Object value) {
        this.vendor1099 = value;
    }

    /**
     * Sets id
     * 
     * @param value Value to set
     */
    public void setId(Object value) {
        this.id = value;
    }

    /**
     * Sets showAs
     * 
     * @param value Value to set
     */
    public void setShowAs(Object value) {
        this.showAs = value;
    }

    /**
     * Sets address
     * 
     * @param value Value to set
     */
    public void setAddress(Object value) {
        this.address = value;
    }

    /**
     * Sets email
     * 
     * @param value Value to set
     */
    public void setEmail(Object value) {
        this.email = value;
    }

    /**
     * Sets webSite
     * 
     * @param value Value to set
     */
    public void setWebSite(Object value) {
        this.webSite = value;
    }

    /**
     * Sets name
     * 
     * @param value Value to set
     */
    public void setName(Object value) {
        this.name = value;
    }

    /**
     * Get all superclasses and interfaces recursively.
     * 
     * @param classes List of classes to which to add all found super classes and interfaces.
     * @param clazz   The class to start the search with.
     */
    private void computeClassHierarchy(Class clazz, List classes) {
        for (Class current = clazz; (current!= null); current = current.getSuperclass()) {
            if (classes.contains(current)) {
                return ;
            }
            classes.add(current);
            for (Class currentInterface: current.getInterfaces()) {
                computeClassHierarchy(currentInterface, classes);
            }
        }
    }

    /**
     * Checks whether the specified class parameter is an instance of {@link List }
     * 
     * @param clazz <code>Class</code> to check.
     * @return
     */
    private boolean isListClass(Class clazz) {
        List<Class> classes = new ArrayList<Class>();
        computeClassHierarchy(clazz, classes);
        return classes.contains(List.class);
    }

    /**
     * Checks whether the specified class parameter is an instance of {@link Map }
     * 
     * @param clazz <code>Class</code> to check.
     * @return
     */
    private boolean isMapClass(Class clazz) {
        List<Class> classes = new ArrayList<Class>();
        computeClassHierarchy(clazz, classes);
        return classes.contains(Map.class);
    }

    private boolean isList(Type type) {
        if ((type instanceof Class)&&isListClass(((Class) type))) {
            return true;
        }
        if (type instanceof ParameterizedType) {
            return isList(((ParameterizedType) type).getRawType());
        }
        if (type instanceof WildcardType) {
            Type[] upperBounds = ((WildcardType) type).getUpperBounds();
            return ((upperBounds.length!= 0)&&isList(upperBounds[ 0 ]));
        }
        return false;
    }

    private boolean isMap(Type type) {
        if ((type instanceof Class)&&isMapClass(((Class) type))) {
            return true;
        }
        if (type instanceof ParameterizedType) {
            return isMap(((ParameterizedType) type).getRawType());
        }
        if (type instanceof WildcardType) {
            Type[] upperBounds = ((WildcardType) type).getUpperBounds();
            return ((upperBounds.length!= 0)&&isMap(upperBounds[ 0 ]));
        }
        return false;
    }

    private boolean isAssignableFrom(Type expectedType, Class clazz) {
        if (expectedType instanceof Class) {
            return ((Class) expectedType).isAssignableFrom(clazz);
        }
        if (expectedType instanceof ParameterizedType) {
            return isAssignableFrom(((ParameterizedType) expectedType).getRawType(), clazz);
        }
        if (expectedType instanceof WildcardType) {
            Type[] upperBounds = ((WildcardType) expectedType).getUpperBounds();
            if (upperBounds.length!= 0) {
                return isAssignableFrom(upperBounds[ 0 ], clazz);
            }
        }
        return false;
    }

    private Object evaluate(MuleMessage muleMessage, Object source) {
        if (source instanceof String) {
            String stringSource = ((String) source);
            if (stringSource.startsWith(patternInfo.getPrefix())&&stringSource.endsWith(patternInfo.getSuffix())) {
                return expressionManager.evaluate(stringSource, muleMessage);
            } else {
                return expressionManager.parse(stringSource, muleMessage);
            }
        }
        return source;
    }

    private Object evaluateAndTransform(MuleMessage muleMessage, Type expectedType, Object source)
        throws TransformerException
    {
        if (source == null) {
            return source;
        }
        Object target = null;
        if (isList(source.getClass())) {
            if (isList(expectedType)) {
                List newList = new ArrayList();
                Type valueType = ((ParameterizedType) expectedType).getActualTypeArguments()[ 0 ];
                ListIterator iterator = ((List) source).listIterator();
                while (iterator.hasNext()) {
                    Object subTarget = iterator.next();
                    newList.add(evaluateAndTransform(muleMessage, valueType, subTarget));
                }
                target = newList;
            } else {
                target = source;
            }
        } else {
            if (isMap(source.getClass())) {
                if (isMap(expectedType)) {
                    Type keyType = Object.class;
                    Type valueType = Object.class;
                    if (expectedType instanceof ParameterizedType) {
                        keyType = ((ParameterizedType) expectedType).getActualTypeArguments()[ 0 ];
                        valueType = ((ParameterizedType) expectedType).getActualTypeArguments()[ 1 ];
                    }
                    Map map = ((Map) source);
                    Map newMap = new HashMap();
                    for (Object entryObj: map.entrySet()) {
                        {
                            Map.Entry entry = ((Map.Entry) entryObj);
                            Object newKey = evaluateAndTransform(muleMessage, keyType, entry.getKey());
                            Object newValue = evaluateAndTransform(muleMessage, valueType, entry.getValue());
                            newMap.put(newKey, newValue);
                        }
                    }
                    target = newMap;
                } else {
                    target = source;
                }
            } else {
                target = evaluate(muleMessage, source);
            }
        }
        if ((target!= null)&&(!isAssignableFrom(expectedType, target.getClass()))) {
            DataType sourceDataType = DataTypeFactory.create(target.getClass());
            DataType targetDataType = DataTypeFactory.create(((Class) expectedType));
            Transformer t = muleContext.getRegistry().lookupTransformer(sourceDataType, targetDataType);
            return t.transform(target);
        } else {
            return target;
        }
    }

    /**
     * Invokes the MessageProcessor.
     * 
     * @param event MuleEvent to be processed
     * @throws MuleException
     */
    public MuleEvent process(MuleEvent event)
        throws MuleException
    {
        MuleMessage muleMessage = event.getMessage();
        QuickBooksModuleOAuth1Adapter castedModuleObject = null;
        if (moduleObject instanceof String) {
            castedModuleObject = ((QuickBooksModuleOAuth1Adapter) muleContext.getRegistry().lookupObject(((String) moduleObject)));
            if (castedModuleObject == null) {
                throw new MessagingException(CoreMessages.failedToCreate("updateVendor"), event, new RuntimeException("Cannot find the configuration specified by the config-ref attribute."));
            }
        } else {
            castedModuleObject = ((QuickBooksModuleOAuth1Adapter) moduleObject);
        }
        if (castedModuleObject.getOauthVerifier() == null) {
            String authorizationUrl = castedModuleObject.getAuthorizationUrl();
            event.getMessage().setOutboundProperty("http.status", "302");
            event.getMessage().setOutboundProperty("Location", authorizationUrl);
            return event;
        }
        if (castedModuleObject.getAccessToken() == null) {
            castedModuleObject.fetchAccessToken();
        }
        try {
            String accessToken = castedModuleObject.getAccessToken();
            String accessTokenSecret = castedModuleObject.getAccessTokenSecret();
            Map<String, Object> transformedId = ((Map<String, Object> ) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_idType").getGenericType(), id));
            String transformedSyncToken = ((String) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_syncTokenType").getGenericType(), syncToken));
            String transformedName = ((String) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_nameType").getGenericType(), name));
            String transformedGivenName = ((String) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_givenNameType").getGenericType(), givenName));
            String transformedMiddleName = ((String) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_middleNameType").getGenericType(), middleName));
            String transformedFamilyName = ((String) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_familyNameType").getGenericType(), familyName));
            String transformedDBAName = ((String) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_dBANameType").getGenericType(), dBAName));
            String transformedShowAs = ((String) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_showAsType").getGenericType(), showAs));
            List<Map<String, Object>> transformedWebSite = ((List<Map<String, Object>> ) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_webSiteType").getGenericType(), webSite));
            Integer transformedTaxIdentifier = ((Integer) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_taxIdentifierType").getGenericType(), taxIdentifier));
            String transformedAcctNum = ((String) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_acctNumType").getGenericType(), acctNum));
            Boolean transformedVendor1099 = ((Boolean) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_vendor1099Type").getGenericType(), vendor1099));
            List<String> transformedEmail = ((List<String> ) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_emailType").getGenericType(), email));
            List<Map<String, Object>> transformedPhone = ((List<Map<String, Object>> ) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_phoneType").getGenericType(), phone));
            List<Map<String, Object>> transformedAddress = ((List<Map<String, Object>> ) evaluateAndTransform(muleMessage, UpdateVendorMessageProcessor.class.getDeclaredField("_addressType").getGenericType(), address));
            retryCount.getAndIncrement();
            Object resultPayload;
            resultPayload = castedModuleObject.updateVendor(accessToken, accessTokenSecret, transformedId, transformedSyncToken, transformedName, transformedGivenName, transformedMiddleName, transformedFamilyName, transformedDBAName, transformedShowAs, transformedWebSite, transformedTaxIdentifier, transformedAcctNum, transformedVendor1099, transformedEmail, transformedPhone, transformedAddress);
            if (resultPayload == null) {
                event = new DefaultMuleEvent(new DefaultMuleMessage(NullPayload.getInstance(), muleContext), event);
            } else {
                List<Transformer> transformerList;
                transformerList = new ArrayList<Transformer>();
                transformerList.add(new TransformerTemplate(new TransformerTemplate.OverwitePayloadCallback(resultPayload)));
                event.getMessage().applyTransformers(event, transformerList);
            }
            retryCount.set(0);
            return event;
        } catch (Exception e) {
            throw new MessagingException(CoreMessages.failedToInvoke("updateVendor"), event, e);
        }
    }

}
