/**
 * Copyright (c) 2017 Uber Technologies, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.uber.bundle.gson;

import android.os.Parcelable;
import android.util.SparseArray;

import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.uber.bundle.extension.Deserializer;

import java.util.ArrayList;

import javax.lang.model.element.VariableElement;

/**
 * Deserializes objects using Gson
 */
public class GsonDeserializer implements Deserializer {

    public static final TypeName GSON = ClassName.get(Gson.class);

    // GSON type token
    private static final String TYPE_TOKEN_FULLY_QUALIFIED = TypeToken.class.getCanonicalName();

    private static final ClassName ARRAY_LIST_CLASS = ClassName.get(ArrayList.class);

    // Android-specific classes
    private static final ClassName SPARSE_ARRAY_CLASS = ClassName.get(SparseArray.class);
    private static final TypeName PARCELABLE = ClassName.get(Parcelable.class);

    @Override
    public String deserializeSimpleObject(
            String readFromBundle,
            ClassName className,
            VariableElement deserializerElement) {
        return CodeBlock.builder()
                .add("$L.$L($L, $L.class)", deserializerElement.getSimpleName().toString(), "fromJson",
                        readFromBundle, className.simpleName())
                .build()
                .toString();
    }

    @Override
    public String deserializeEnum(
            String readFromBundle,
            VariableElement deserializerElement) {
        return CodeBlock.builder()
                .add("$L.$L($L)", deserializerElement.getSimpleName().toString(), "fromJson", readFromBundle)
                .build()
                .toString();
    }

    @Override
    public String deserializeArrayOfObjects(
            String readFromBundle,
            ClassName className,
            VariableElement deserializerElement) {
        return CodeBlock.builder()
                .add("$L.$L($L, $L[].class)", deserializerElement.getSimpleName().toString(), "fromJson",
                        readFromBundle, className.simpleName())
                .build()
                .toString();
    }

    @Override
    public String deserializeSparseArrayListOfParcelable(
            String readFromBundle,
            VariableElement deserializerElement) {
        String typeToken = "new " + TYPE_TOKEN_FULLY_QUALIFIED + "<" + SPARSE_ARRAY_CLASS + "<"
                + ((ClassName) PARCELABLE).simpleName() + ">>(){}" + ".getType()";
        return deserializerElement + ".fromJson(" + readFromBundle + ", " + typeToken + ")";
    }

    @Override
    public String deserializeArrayListOfObjects(
            String readFromBundle,
            ClassName className,
            VariableElement deserializerElement) {
        return CodeBlock.builder()
                .add("$L.$L($L, new $L<$L<$L>>(){}.getType())", deserializerElement.getSimpleName().toString(),
                        "fromJson",
                        readFromBundle, TYPE_TOKEN_FULLY_QUALIFIED, ARRAY_LIST_CLASS, className.simpleName())
                .build()
                .toString();
    }

    @Override
    public String deserializeUnknownObject(
            String readFromBundle,
            ClassName className,
            VariableElement deserializerElement) {
        return CodeBlock.builder()
                .add("$L.$L($L, new $L<$L>(){}.getType())", deserializerElement.getSimpleName().toString(), "fromJson",
                        readFromBundle, TYPE_TOKEN_FULLY_QUALIFIED, className.simpleName())
                .build()
                .toString();
    }

    @Override
    public String deserializeUnknownParameterizedObject(
            String readFromBundle,
            ParameterizedTypeName parameterizedTypeName,
            VariableElement deserializerElement) {
        StringBuilder typeToken = new StringBuilder("new ")
                .append(TYPE_TOKEN_FULLY_QUALIFIED)
                .append("<")
                .append(parameterizedTypeName.rawType.simpleName())
                .append("<");
        for (TypeName typeName : parameterizedTypeName.typeArguments) {
            typeToken.append(deserializeParameterizedObject(typeName))
                    .append(", ");
        }
        typeToken.deleteCharAt(typeToken.length() - 1);
        typeToken.deleteCharAt(typeToken.length() - 1);

        typeToken.append(">(){}.getType()");

        return CodeBlock.builder()
                .add("$L.$L($L, $L)", deserializerElement.getSimpleName().toString(), "fromJson", readFromBundle,
                        typeToken)
                .build()
                .toString();
    }

    private String deserializeParameterizedObject(TypeName typeName) {
        if (typeName instanceof ParameterizedTypeName) {
            ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName) typeName;
            StringBuilder typeToken = new StringBuilder("<")
                    .append(parameterizedTypeName.rawType.simpleName());
            for (TypeName internalTypeName : parameterizedTypeName.typeArguments) {
                typeToken.append(deserializeParameterizedObject(internalTypeName))
                        .append(", ");
            }
            return typeToken.deleteCharAt(typeToken.length() - 1)
                    .deleteCharAt(typeToken.length() - 1)
                    .append(">")
                    .toString();
        } else {
            return ((ClassName) typeName).simpleName() + ">";
        }
    }
}