/*
 * Copyright 2014, The Sporting Exchange Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package uk.co.exemel.disco.transport.impl.protocol.http.soap;

import uk.co.exemel.disco.core.api.exception.DiscoMarshallingException;
import uk.co.exemel.disco.core.api.exception.DiscoValidationException;
import uk.co.exemel.disco.core.api.transcription.Parameter;
import uk.co.exemel.disco.core.api.transcription.ParameterType;
import uk.co.exemel.disco.core.api.transcription.Transcribable;
import uk.co.exemel.disco.core.api.transcription.TranscribableParams;
import uk.co.exemel.disco.core.api.transcription.TranscriptionInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.co.exemel.disco.util.dates.DateTimeUtility;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.util.base64.Base64Utils;

import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Transcribes from XML to types supported by BISDL or Transcribable Java objects.
 */
@SuppressWarnings("unchecked")
public class XMLTranscriptionInput implements TranscriptionInput {

    private static final Logger LOGGER = LoggerFactory.getLogger(XMLTranscriptionInput.class);

    private static final QName keyAttName = new QName("key");

    private OMElement currentNode;

    public XMLTranscriptionInput(OMElement currentNode) {
        this.currentNode = currentNode;
    }

    public Object readObject(Parameter param, boolean client) throws Exception {
        Iterator iterator = currentNode.getChildrenWithLocalName(param.getName());
        if (!iterator.hasNext()) {
            return null;
        }

        return readObject(param.getParameterType(), (OMElement)iterator.next(), client);
    }

    private Object readObject(ParameterType paramType, OMElement node, boolean client) throws Exception {
        switch (paramType.getType()) {
            case BOOLEAN:
            case DOUBLE:
            case FLOAT:
            case INT:
            case LONG:
            case STRING:
            case ENUM:
            case DATE:
            case BYTE:
                return node == null ? null : readSimpleObject(paramType, node.getLocalName(), node.getText(), client);
            case OBJECT:
                //descend - note possibly two levels if inside a collection recursion
                OMElement _copy = this.currentNode;
                currentNode = node;

                Transcribable t = (Transcribable)paramType.getImplementationClass().newInstance();
                t.transcribe(this, TranscribableParams.getAll(), client);

                //ascend
                this.currentNode = _copy;
                return t;
            case MAP:
                Map map = new HashMap();
                for (Iterator i = node.getChildElements(); i.hasNext();) {
                    OMElement element = (OMElement)i.next();
                    Object key = readSimpleObject(paramType.getComponentTypes()[0],  node.getLocalName(), element.getAttributeValue(keyAttName), client);
                    map.put(key, readObject(paramType.getComponentTypes()[1], (OMElement)element.getChildElements().next(), client));
                }
                return map;
            case LIST:
                if (paramType.getComponentTypes()[0].getType() == ParameterType.Type.BYTE) {
                    try {
                        return Base64Utils.decode(node.getText());
                    } catch (Exception e) {
                        String message = "Unable to parse " + node.getText() + " as type " + paramType;
                        LOGGER.debug(message, e);
                        throw DiscoMarshallingException.unmarshallingException("soap",message,e,client);
                    }
                } else {
                    List list = new ArrayList();
                    for (Iterator i = node.getChildElements(); i.hasNext();) {
                        list.add(readObject(paramType.getComponentTypes()[0], (OMElement)i.next(),client));
                    }
                    return list;
                }
            case SET:
                Set set = new HashSet();
                for (Iterator i = node.getChildElements(); i.hasNext();) {
                    set.add(readObject(paramType.getComponentTypes()[0], (OMElement)i.next(),client));
                }
                return set;
        }
        return null;
    }

    private Object readSimpleObject(ParameterType paramType, String paramName, String textValue, boolean client) {
        try {
            switch (paramType.getType()) {
                case BOOLEAN:
                    if (textValue.equalsIgnoreCase("true")) {
                        return true;
                    }
                    if (textValue.equalsIgnoreCase("false")) {
                        return false;
                    }
                    throw new IllegalArgumentException();
                case DOUBLE:
                    return Double.valueOf(textValue);
                case FLOAT:
                    return Float.valueOf(textValue);
                case INT:
                    return Integer.valueOf(textValue);
                case LONG:
                    return Long.valueOf(textValue);
                case STRING:
                    return textValue;
                case ENUM:
                    // this is converted to an enum further down in the Transcribable implementation so the original raw value can be stored in a soft failure..
                    return textValue;
                case DATE:
                    return DateTimeUtility.parse(textValue);
                case BYTE:
                    return Byte.valueOf(textValue);
            }
        } catch (Exception e) {
            throw exceptionDuringDeserialisation(paramType, paramName, e, client);
        }
        throw new UnsupportedOperationException("Parameter Type " + paramType + " is not supported as a simple object type");
    }

    public static DiscoValidationException exceptionDuringDeserialisation(ParameterType paramType, String paramName, Exception e, boolean client) {
        StringBuilder logBuffer = new StringBuilder();
        logBuffer.append("Unable to convert data in request to ");
        logBuffer.append(paramType.getType().name());
        logBuffer.append(" for parameter: ");
        logBuffer.append(paramName);
        String message = logBuffer.toString();

        LOGGER.debug(message , e);
        throw DiscoMarshallingException.unmarshallingException("xml", message, e, client);

    }
}
