package com.github.tinselspoon.intellij.kubernetes;

import java.util.Collections;
import java.util.Map.Entry;

import javax.swing.Icon;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.yaml.YAMLLanguage;
import org.jetbrains.yaml.psi.YAMLDocument;
import org.jetbrains.yaml.psi.YAMLKeyValue;
import org.jetbrains.yaml.psi.YAMLMapping;

import com.github.tinselspoon.intellij.kubernetes.model.FieldType;
import com.github.tinselspoon.intellij.kubernetes.model.Model;
import com.github.tinselspoon.intellij.kubernetes.model.ModelProvider;
import com.github.tinselspoon.intellij.kubernetes.model.Property;
import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionProvider;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.icons.AllIcons.Json;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorModificationUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.PlatformIcons;
import com.intellij.util.ProcessingContext;

/**
 * Completion contributor for Kubernetes YAML files.
 */
public class KubernetesYamlCompletionContributor extends CompletionContributor {

    /** Default constructor. */
    public KubernetesYamlCompletionContributor() {
        extend(CompletionType.BASIC, PlatformPatterns.psiElement().withLanguage(YAMLLanguage.INSTANCE), new Provider());
    }

    /**
     * Adds suggestions for possible items to insert under the value of a given {@link YAMLKeyValue}.
     *
     * @param modelProvider the store for model info.
     * @param resultSet the result set to append suggestions to.
     * @param resourceKey the identifier of the resource in question.
     * @param keyValue the {@link YAMLKeyValue} to obtain suggestions for.
     */
    private static void addValueSuggestionsForKey(@NotNull final ModelProvider modelProvider, final @NotNull CompletionResultSet resultSet, @NotNull final ResourceTypeKey resourceKey,
            @NotNull final YAMLKeyValue keyValue) {
        final Property keyProperty = KubernetesYamlPsiUtil.propertyForKey(modelProvider, resourceKey, keyValue);
        final Model keyModel = KubernetesYamlPsiUtil.modelForKey(modelProvider, resourceKey, keyValue);
        if (keyProperty != null && keyProperty.getType() == FieldType.BOOLEAN) {
            resultSet.addElement(LookupElementBuilder.create("true").withBoldness(true));
            resultSet.addElement(LookupElementBuilder.create("false").withBoldness(true));
        }
        if (keyModel != null) {
            for (final Entry<String, Property> property : keyModel.getProperties().entrySet()) {
                resultSet.addElement(createKeyLookupElement(property.getKey(), property.getValue()));
            }
        }
    }

    /**
     * Create a {@link LookupElementBuilder} for completing the text of a key. Do not use when completing a value.
     *
     * @param completionObject the object to pass to {@link LookupElementBuilder#create(Object)}.
     * @param addLayerOfNesting whether a newline and indent should be added when accepting the completed value - this is used when inserting the value will introduce a level of nesting (i.e. for an
     * object or array type).
     * @return the created {@code LookupElementBuilder}.
     */
    private static LookupElementBuilder createKeyLookupElement(@NotNull final Object completionObject, final boolean addLayerOfNesting) {
        return LookupElementBuilder.create(completionObject).withInsertHandler((insertionContext, lookupElement) -> {
            // If the caret is at the end of the line, add in the property colon when completing
            if (insertionContext.getCompletionChar() != ':' && insertionContext.getCompletionChar() != ' ') {
                final Editor editor = insertionContext.getEditor();
                final int offset = editor.getCaretModel().getOffset();
                final int lineNumber = editor.getDocument().getLineNumber(offset);
                final int lineEndOffset = editor.getDocument().getLineEndOffset(lineNumber);
                if (lineEndOffset == offset) {
                    final String autocompleteString;
                    if (addLayerOfNesting) {
                        // Copy the indentation characters present on this line, and add one additional level of indentation
                        final int lineStartOffset = editor.getDocument().getLineStartOffset(lineNumber);
                        final String lineContent = editor.getDocument().getText().substring(lineStartOffset, lineEndOffset);
                        final int offsetOfContent = lineContent.length() - StringUtil.trimLeading(lineContent).length();
                        final String indentToLine = lineContent.substring(0, offsetOfContent);
                        final CodeStyleSettings currentSettings = CodeStyleSettingsManager.getSettings(insertionContext.getProject());
                        final CommonCodeStyleSettings.IndentOptions indentOptions = currentSettings.getIndentOptions(insertionContext.getFile().getFileType());
                        final String additionalIndent = indentOptions.USE_TAB_CHARACTER ? "\t" : StringUtil.repeatSymbol(' ', indentOptions.INDENT_SIZE);
                        autocompleteString = ":\n" + indentToLine + additionalIndent;
                    } else {
                        autocompleteString = ": ";
                    }
                    EditorModificationUtil.insertStringAtCaret(editor, autocompleteString);
                }
            }
        });
    }

    /**
     * Create a {@link LookupElementBuilder} when completing the text of a key identified by the given name and definition.
     *
     * @param propertyName the name of the property.
     * @param propertySpec the schema definition of the property.
     * @return the created {@code LookupElementBuilder}.
     */
    @NotNull
    private static LookupElementBuilder createKeyLookupElement(@NotNull final String propertyName, @NotNull final Property propertySpec) {
        final String typeText = ModelUtil.typeStringFor(propertySpec);
        Icon icon = PlatformIcons.PROPERTY_ICON;
        boolean addLayerOfNesting = false;
        if (propertySpec.getType() == FieldType.ARRAY) {
            icon = Json.Array;
            addLayerOfNesting = true;
        } else if (propertySpec.getRef() != null || propertySpec.getType() == FieldType.OBJECT) {
            icon = Json.Object;
            addLayerOfNesting = true;
        }
        return createKeyLookupElement(new PropertyCompletionItem(propertyName, propertySpec), addLayerOfNesting).withTypeText(typeText, true).withIcon(icon);
    }

    /**
     * Gets whether a given {@link YAMLKeyValue} is at the root level of the document.
     *
     * @param keyValue the element to evaluate.
     * @return {@code true} if this is a top-level mapping; otherwise, {@code false}.
     */
    private static boolean isTopLevelMapping(final YAMLKeyValue keyValue) {
        return keyValue.getParentMapping() != null && keyValue.getParentMapping().getParent() instanceof YAMLDocument;
    }

    /** The main actor in generating completion suggestions. */
    private static class Provider extends CompletionProvider<CompletionParameters> {
        @Override
        protected void addCompletions(@NotNull final CompletionParameters completionParameters, final ProcessingContext processingContext, @NotNull final CompletionResultSet resultSet) {
            // Make sure we are actually in a document that resembles a Kubernetes resource before offering completion
            final PsiElement element = completionParameters.getPosition();
            if (!KubernetesYamlPsiUtil.isKubernetesFile(element) || element instanceof PsiComment) {
                return;
            }

            // Get the current key/value being worked on
            final ModelProvider modelProvider = ModelProvider.INSTANCE;
            final YAMLMapping topLevelMapping = KubernetesYamlPsiUtil.getTopLevelMapping(element);
            final YAMLKeyValue keyValue = PsiTreeUtil.getParentOfType(element, YAMLKeyValue.class);

            // Try and find the resource key which will aid our completion
            final ResourceTypeKey resourceKey = KubernetesYamlPsiUtil.findResourceKey(element);

            // We must be at the very top level if there is no enclosing keyValue
            if (keyValue == null) {
                if (resourceKey == null) {
                    // If we don't know what the resource type is, add the "apiVersion" and "kind" fields which will be applicable to all resources
                    resultSet.addElement(createKeyLookupElement("apiVersion", false));
                    resultSet.addElement(createKeyLookupElement("kind", false));
                } else {
                    // If we do know the resource type, add the fields relevant to that resource
                    modelProvider.findProperties(resourceKey, Collections.emptyList()).forEach((key, value) -> resultSet.addElement(createKeyLookupElement(key, value)));
                }
            } else {
                // The "apiVersion" and "kind" fields on the top level are special cases where we have to calculate the completion
                if (isTopLevelMapping(keyValue)) {
                    if ("apiVersion".equals(keyValue.getKeyText())) {
                        for (final String apiVersion : modelProvider.suggestApiVersions()) {
                            resultSet.addElement(LookupElementBuilder.create(apiVersion).withIcon(PlatformIcons.PACKAGE_ICON));
                        }
                    } else if ("kind".equals(keyValue.getKeyText())) {
                        final String apiVersion = KubernetesYamlPsiUtil.getValueText(topLevelMapping, "apiVersion");
                        for (final ResourceTypeKey kind : modelProvider.suggestKinds(apiVersion)) {
                            final String kindApiVersion = kind.getApiVersion();
                            // Add on the apiVersion
                            resultSet.addElement(LookupElementBuilder.create(kind.getKind())
                                                                     .withTypeText(kindApiVersion, true)
                                                                     .withIcon(PlatformIcons.CLASS_ICON)
                                                                     .withInsertHandler((insertionContext, lookupElement) -> {
                                                                         if (topLevelMapping == null || topLevelMapping.getKeyValueByKey("apiVersion") == null) {
                                                                             EditorModificationUtil.insertStringAtCaret(insertionContext.getEditor(), "\napiVersion: " + kindApiVersion + "\n");
                                                                         }
                                                                     }));
                        }
                    }
                }

                if (resourceKey != null) {
                    addValueSuggestionsForKey(modelProvider, resultSet, resourceKey, keyValue);
                }
            }
        }
    }
}