/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.generator.openapi;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;

import com.squareup.javapoet.MethodSpec;

import static org.apache.commons.lang3.ClassUtils.isPrimitiveOrWrapper;

class MethodBodySourceCodeEmitter implements CodeEmitter<MethodSpec> {

    private final MethodSpec.Builder builder;

    private boolean first = true;

    private final Deque<Integer> indentIntentStack = new ArrayDeque<>();

    private final Deque<Integer> indentStack = new ArrayDeque<>();

    MethodBodySourceCodeEmitter(final MethodSpec.Builder builder) {
        this.builder = builder;
        indentStack.push(0);
    }

    @Override
    public CodeEmitter<MethodSpec> emit(final String method, final Object... args) {
        final boolean hasArgs = args != null && args.length > 0;

        final int indent = indentLevelOf(method);

        if (!first || indent == 0) {
            builder.addCode("\n");
        }

        builder.addCode(String.join("", Collections.nCopies(indentStack.peek(), "$<")));
        builder.addCode(String.join("", Collections.nCopies(indent, "$>")));

        if (!first) {
            builder.addCode(".");
        }

        indentStack.push(indent);

        if (hasArgs) {
            builder.addCode("$L(" + invocationLiteralsFor(args) + ")", extend(method, argumentsFor(args)));
        } else {
            builder.addCode("$L()", method);
        }

        first = false;

        return this;
    }

    public void endEmit() {
        builder.addCode(";\n");
        first = true;
    }

    @Override
    public MethodSpec result() {
        builder.addCode(String.join("", Collections.nCopies(indentStack.peek(), "$<")));
        builder.addCode("\n");
        return builder.build();
    }

    int indentLevelOf(final String method) {
        switch (method) {
            case "rest":
                return 0;
            case "post":
            case "get":
            case "put":
            case "patch":
            case "delete":
            case "head":
            case "options":
                return 1;
            case "param":
                indentIntentStack.push(3);
                return 2;
            case "endParam":
                indentIntentStack.pop();
                return 2;
            case "route":
                indentIntentStack.push(3);
                return 2;
            case "endRest":
                indentIntentStack.pop();
                return 2;
            default:
                if (indentIntentStack.isEmpty()) {
                    return 2;
                }
                return indentIntentStack.peek();
        }
    }

    String invocationLiteralsFor(final Object[] args) {
        final StringJoiner literals = new StringJoiner(",");

        for (final Object arg : args) {
            if (isPrimitiveOrWrapper(arg.getClass())) {
                literals.add("$L");
            } else if (arg instanceof String) {
                literals.add("$S");
            } else if (arg instanceof Enum) {
                literals.add("$T.$L");
            } else if (arg instanceof String[]) {
                literals.add("$S");
            }
        }

        return literals.toString();
    }

    static Object[] argumentsFor(final Object[] args) {
        final List<Object> arguments = new ArrayList<>(args.length);

        for (final Object arg : args) {
            if (isPrimitiveOrWrapper(arg.getClass())) {
                arguments.add(arg);
            } else if (arg instanceof String) {
                arguments.add(arg);
            } else if (arg instanceof Enum) {
                arguments.add(arg.getClass());
                arguments.add(arg);
            } else if (arg instanceof String[]) {
                arguments.add(Arrays.stream((String[]) arg).collect(Collectors.joining(",")));
            }
        }

        return arguments.toArray(new Object[arguments.size()]);
    }

    static Object[] extend(final Object first, final Object... others) {
        if (others == null || others.length == 0) {
            return new Object[] { first };
        }

        final Object[] ret = new Object[1 + others.length];

        ret[0] = first;
        System.arraycopy(others, 0, ret, 1, others.length);

        return ret;
    }

}
