/*
 * Copyright 2019 Igor Maznitsa.
 *
 * 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 com.igormaznitsa.mvngolang.utils;

import com.igormaznitsa.meta.annotation.MustNotContainNull;
import com.igormaznitsa.meta.common.utils.Assertions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public final class GoMod {

    private static final Pattern TOKENIZER = Pattern.compile("(\\/\\/|\\\"[^\\\"]+\\\"|[<>=\\w.+\\-/]+|[<>=-]+|\\(|\\)|[\\s\\n]+)");

    @Nonnull
    private static String quoteIfHasSpace(@Nonnull final String str) {
        return str.contains(" ") ? '\"' + str + '\"' : str;
    }

    @Nonnull
    private static String ensureNoQuoting(@Nonnull final String text) {
        return text.startsWith("\"") ? text.substring(1, text.length() - 1) : text;
    }

    @Nonnull
    @MustNotContainNull
    private static List<ModuleInfo> extractModuleInfo(@Nonnull @MustNotContainNull final List<String> tokens, @Nonnull @MustNotContainNull final String... separators) {
        final List<ModuleInfo> result = new ArrayList<>();
        final List<String> tokenBuffer = new ArrayList<>(tokens);
        final List<String> accum = new ArrayList<>();

        while (!tokenBuffer.isEmpty()) {
            final String next = tokenBuffer.remove(0);
            boolean separator = false;
            for (final String s : separators) {
                if (s.equals(next)) {
                    separator = true;
                    break;
                }
            }
            if (separator) {
                switch (accum.size()) {
                    case 1: {
                        result.add(new ModuleInfo(accum.remove(0)));
                    }
                    break;
                    case 2: {
                        final String name = accum.remove(0);
                        final String version = accum.remove(0);
                        result.add(new ModuleInfo(name, version));
                    }
                    break;
                    default:
                        throw new IllegalArgumentException("Can't extract module info from tokens: " + tokens);
                }
            } else {
                accum.add(next);
            }
        }

        switch (accum.size()) {
            case 1: {
                result.add(new ModuleInfo(accum.remove(0)));
            }
            break;
            case 2: {
                final String name = accum.remove(0);
                final String version = accum.remove(0);
                result.add(new ModuleInfo(name, version));
            }
            break;
            default:
                throw new IllegalArgumentException("Can't extract module info from tokens: " + tokens);
        }

        return result;
    }

    @Override
    public boolean equals(@Nullable final Object that) {
        if (that == null) {
            return false;
        }
        if (that == this) {
            return true;
        }
        
        if (that instanceof GoMod) {
            final GoMod thatGoMod = (GoMod) that;
            if (this.items.size()!= thatGoMod.items.size()) return false;
            for(int i=0;i<this.items.size();i++){
                if (!this.items.get(i).equals(thatGoMod.items.get(i))) return false;
            }
            return true;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return 31;
    }

    @Nonnull
    public static GoMod from(@Nonnull final String str) {
        final List<GoModItem> foundItems = new ArrayList<>();

        final Matcher matcher = TOKENIZER.matcher(str);

        ParserState state = ParserState.FIND;

        boolean findEol = false;
        boolean bracket = false;

        final List<String> tokenList = new ArrayList<>();

        String customTokenName = null;

        while (matcher.find()) {
            final String token = matcher.group(1);
            if (findEol) {
                if (token.contains("\n")) {
                    findEol = false;
                    state = bracket ? state : ParserState.FIND;
                }
            } else {
                switch (state) {
                    case FIND: {
                        tokenList.clear();
                        switch (token) {
                            case "module": {
                                state = ParserState.MODULE;
                            }
                            break;
                            case "exclude": {
                                state = ParserState.EXCLUDE;
                            }
                            break;
                            case "replace": {
                                state = ParserState.REPLACE;
                            }
                            break;
                            case "require": {
                                state = ParserState.REQUIRE;
                            }
                            break;
                            default: {
                                if ("//".equals(token)) {
                                    findEol = true;
                                } else if (!token.trim().isEmpty()) {
                                    state = ParserState.CUSTOM;
                                    customTokenName = token;
                                }
                            }
                            break;
                        }
                    }
                    break;
                    case CUSTOM: {
                        if ("//".equals(token)) {
                            if (!bracket) {
                                foundItems.add(new GoCustom(customTokenName, tokenList.toArray(new String[0])));
                                foundItems.clear();
                                customTokenName = null;
                                state = ParserState.FIND;
                            }
                        } else {
                            if ("(".equals(token)) {
                                if (bracket) {
                                    throw new IllegalArgumentException("Duplicated opening bracket in " + state);
                                }
                                bracket = true;
                            } else if (")".equals(token)) {
                                if (!bracket) {
                                    throw new IllegalArgumentException("Unexpected closing bracket in " + state);
                                }
                                bracket = false;
                                foundItems.add(new GoCustom(customTokenName, tokenList.toArray(new String[0])));
                                foundItems.clear();
                                customTokenName = null;
                                state = ParserState.FIND;
                            } else if (token.contains("\n")) {
                                if (!bracket) {
                                    state = ParserState.FIND;
                                    foundItems.add(new GoCustom(customTokenName, tokenList.toArray(new String[0])));
                                }
                            } else {
                                if (!token.trim().isEmpty()) {
                                    tokenList.add(token);
                                }
                            }
                        }
                    }
                    break;
                    case MODULE:
                    case EXCLUDE:
                    case REPLACE:
                    case REQUIRE: {
                        if ("(".equals(token)) {
                            if (bracket) {
                                throw new IllegalArgumentException("Duplicated opening bracket in " + state);
                            }
                            if (!tokenList.isEmpty()) {
                                throw new IllegalArgumentException("Unexpected tokens " + tokenList + " before bracket in " + state);
                            }
                            bracket = true;
                        } else {
                            final boolean processTokenList;
                            ParserState nextState = state;

                            if (")".equals(token)) {
                                if (!bracket) {
                                    throw new IllegalArgumentException("Unexpected closing bracket in " + state);
                                }
                                bracket = false;
                                processTokenList = !tokenList.isEmpty();
                                nextState = ParserState.FIND;
                            } else if ("//".equals(token)) {
                                findEol = true;
                                processTokenList = !bracket || !tokenList.isEmpty();
                                nextState = bracket ? state : ParserState.FIND;
                            } else if (token.contains("\n")) {
                                processTokenList = !bracket || !tokenList.isEmpty();
                                nextState = bracket ? state : ParserState.FIND;
                            } else {
                                if (!token.trim().isEmpty()) {
                                    tokenList.add(ensureNoQuoting(token));
                                }
                                processTokenList = false;
                            }

                            if (processTokenList) {
                                switch (state) {
                                    case MODULE: {
                                        final List<ModuleInfo> moduleInfos = extractModuleInfo(tokenList);
                                        tokenList.clear();
                                        while (!moduleInfos.isEmpty()) {
                                            foundItems.add(new GoModule(moduleInfos.remove(0)));
                                        }
                                    }
                                    break;
                                    case REQUIRE: {
                                        final List<ModuleInfo> moduleInfos = extractModuleInfo(tokenList);
                                        tokenList.clear();
                                        while (!moduleInfos.isEmpty()) {
                                            foundItems.add(new GoRequire(moduleInfos.remove(0)));
                                        }
                                    }
                                    break;
                                    case EXCLUDE: {
                                        final List<ModuleInfo> moduleInfos = extractModuleInfo(tokenList);
                                        tokenList.clear();
                                        while (!moduleInfos.isEmpty()) {
                                            foundItems.add(new GoExclude(moduleInfos.remove(0)));
                                        }
                                    }
                                    break;
                                    case REPLACE: {
                                        final List<ModuleInfo> moduleInfos = extractModuleInfo(tokenList, "=>");
                                        tokenList.clear();
                                        while (!moduleInfos.isEmpty()) {
                                            final ModuleInfo from = moduleInfos.remove(0);
                                            if (moduleInfos.isEmpty()) {
                                                throw new IllegalArgumentException("Can't find target in replace");
                                            }
                                            final ModuleInfo to = moduleInfos.remove(0);
                                            foundItems.add(new GoReplace(from, to));
                                        }
                                    }
                                    break;
                                    default:
                                        throw new Error("Unexpected: " + state);
                                }
                            }

                            state = nextState;
                        }
                    }
                    break;
                }
            }
        }

        if (!tokenList.isEmpty()) {
            switch (state) {
                case MODULE: {
                    final List<ModuleInfo> moduleInfos = extractModuleInfo(tokenList);
                    tokenList.clear();
                    while (!moduleInfos.isEmpty()) {
                        foundItems.add(new GoModule(moduleInfos.remove(0)));
                    }
                }
                break;
                case REQUIRE: {
                    final List<ModuleInfo> moduleInfos = extractModuleInfo(tokenList);
                    tokenList.clear();
                    while (!moduleInfos.isEmpty()) {
                        foundItems.add(new GoRequire(moduleInfos.remove(0)));
                    }
                }
                break;
                case EXCLUDE: {
                    final List<ModuleInfo> moduleInfos = extractModuleInfo(tokenList);
                    tokenList.clear();
                    while (!moduleInfos.isEmpty()) {
                        foundItems.add(new GoExclude(moduleInfos.remove(0)));
                    }
                }
                break;
                case REPLACE: {
                    final List<ModuleInfo> moduleInfos = extractModuleInfo(tokenList, "=>");
                    tokenList.clear();
                    while (!moduleInfos.isEmpty()) {
                        final ModuleInfo from = moduleInfos.remove(0);
                        if (moduleInfos.isEmpty()) {
                            throw new IllegalArgumentException("Can't find target in replace");
                        }
                        final ModuleInfo to = moduleInfos.remove(0);
                        foundItems.add(new GoReplace(from, to));
                    }
                }
                break;
                case CUSTOM: {
                    foundItems.add(new GoCustom(customTokenName, tokenList.toArray(new String[0])));
                }
                break;
                default:break;
            }
        }

        return new GoMod(foundItems);
    }
    private final List<GoModItem> items;

    private GoMod(@Nonnull @MustNotContainNull final List<GoModItem> items) {
        final List<GoModItem> newList = new ArrayList<>(items);
        Collections.sort(newList);
        this.items = newList;
    }

    @Nonnull
    public GoMod addItem(@Nonnull final GoModItem item) {
        this.items.add(item);
        Collections.sort(this.items);
        return this;
    }

    @Nonnull
    @MustNotContainNull
    public <T extends GoModItem> List<T> find(@Nonnull final Class<T> klass) {
        final List<T> result = new ArrayList<>();

        for (final GoModItem i : this.items) {
            if (klass == i.getClass()) {
                result.add(klass.cast(i));
            }
        }

        return result;
    }

    public int size() {
        return this.items.size();
    }

    @Nonnull
    @Override
    public String toString() {
        final StringBuilder buffer = new StringBuilder();
        for (final GoModItem i : this.items) {
            if (buffer.length() > 0) {
                buffer.append('\n');
            }
            buffer.append(i.toString());
        }
        return buffer.toString();
    }

    public static abstract class GoModItem implements Comparable<GoModItem> {

        @Override
        public int compareTo(@Nonnull final GoModItem that) {
            int result = Integer.compare(this.getPriority(), that.getPriority());
            if (result == 0) {
                result = this.toString().compareTo(that.toString());
            }
            return result;
        }

        public abstract int getPriority();

        @Override
        public abstract boolean equals(@Nullable Object that);
        
        @Override
        public abstract int hashCode();
    }

    public final static class ModuleInfo {

        private final String name;
        private final String version;

        public ModuleInfo(@Nonnull final String name) {
            this.name = Assertions.assertNotNull(name);
            this.version = null;
        }

        public ModuleInfo(@Nonnull final String name, @Nullable final String version) {
            this.name = Assertions.assertNotNull(name);
            this.version = version;
        }

        @Nonnull
        public String getName() {
            return this.name;
        }

        @Nullable
        public String getVersion() {
            return this.version;
        }

        @Override
        public boolean equals(@Nullable final Object that){
            if (that == null) return false;
            if (that == this) return true;
            
            if (that instanceof ModuleInfo) {
                final ModuleInfo thatModuleInfo = (ModuleInfo) that;
                return this.name.equals(thatModuleInfo.name) && (Objects.equals(this.version, thatModuleInfo.version));
            }
            return false;
        }
        
        @Override
        public int hashCode(){
            return this.name.hashCode() ^ (this.version == null ? 0 : this.version.hashCode());
        }
        
        @Nonnull
        @Override
        public String toString() {
            return quoteIfHasSpace(this.name) + (this.version == null ? "" : " " + quoteIfHasSpace(this.version));
        }
    }

    public static final class GoModule extends GoModItem {

        private final ModuleInfo moduleInfo;

        public GoModule(@Nonnull final ModuleInfo module) {
            this.moduleInfo = Assertions.assertNotNull(module);
        }

        @Override
        public boolean equals(@Nullable final Object that) {
            if (that == null) return false;
            if (that == this) return true;
            
            if (that instanceof GoModule) {
                return this.moduleInfo.equals(((GoModule) that).moduleInfo);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return this.moduleInfo.hashCode();
        }

        @Nonnull
        public ModuleInfo getModuleInfo() {
            return this.moduleInfo;
        }

        @Override
        @Nonnull
        public String toString() {
            return "module " + this.moduleInfo;
        }

        @Override
        public int getPriority() {
            return 0;
        }
    }

    public static class GoRequire extends GoModItem {

        private final ModuleInfo moduleInfo;

        public GoRequire(@Nonnull final ModuleInfo moduleInfo) {
            this.moduleInfo = Assertions.assertNotNull(moduleInfo);
        }

        @Override
        public boolean equals(@Nullable final Object that) {
            if (that == null) {
                return false;
            }
            if (that == this) {
                return true;
            }

            if (that instanceof GoRequire) {
                return this.moduleInfo.equals(((GoRequire) that).moduleInfo);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return this.moduleInfo.hashCode();
        }
        
        @Nonnull
        public ModuleInfo getModuleInfo() {
            return this.moduleInfo;
        }

        @Nonnull
        @Override
        public String toString() {
            return "require " + this.moduleInfo;
        }

        @Override
        public int getPriority() {
            return 1;
        }
    }

    public static final class GoCustom extends GoModItem {

        private final String name;
        private final String[] tokens;

        public GoCustom(@Nonnull final String name, @Nonnull @MustNotContainNull final String[] tokens) {
            this.name = Assertions.assertNotNull(name);
            this.tokens = tokens.clone();
        }

        @Override
        public boolean equals(@Nullable final Object that) {
            if (that == null) {
                return false;
            }
            if (that == this) {
                return true;
            }

            if (that instanceof GoCustom) {
                return this.name.equals(((GoCustom) that).name) && Arrays.equals(this.tokens, ((GoCustom) that).tokens);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return this.name.hashCode();
        }
        
        @Nonnull
        @Override
        public String toString() {
            final StringBuilder result = new StringBuilder();
            result.append(name);
            for (final String t : tokens) {
                if (")".equals(t)) {
                    result.append('\n');
                }
                result.append(' ').append(quoteIfHasSpace(t));
                if ("(".equals(t)) {
                    result.append('\n');
                }
            }
            return result.toString();
        }

        @Override
        public int getPriority() {
            return 100;
        }
    }

    public static final class GoReplace extends GoModItem {

        private final ModuleInfo module;
        private final ModuleInfo replacement;

        public GoReplace(@Nonnull final ModuleInfo module, @Nonnull final ModuleInfo replacement) {
            this.module = Assertions.assertNotNull(module);
            this.replacement = Assertions.assertNotNull(replacement);
        }

        @Override
        public boolean equals(@Nullable final Object that) {
            if (that == null) {
                return false;
            }
            if (that == this) {
                return true;
            }

            if (that instanceof GoReplace) {
                return this.module.equals(((GoReplace) that).module) && this.replacement.equals(((GoReplace) that).replacement);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return this.module.hashCode() ^ this.replacement.hashCode();
        }
        
        @Nonnull
        public ModuleInfo getModule() {
            return this.module;
        }

        @Nonnull
        public ModuleInfo getReplacement() {
            return this.replacement;
        }

        @Nonnull
        @Override
        public String toString() {
            return "replace " + this.module + " => " + this.replacement;
        }

        @Override
        public int getPriority() {
            return 2;
        }

    }

    public static final class GoExclude extends GoModItem {

        private final ModuleInfo module;

        public GoExclude(@Nonnull final ModuleInfo module) {
            this.module = Assertions.assertNotNull(module);
        }

        @Nonnull
        public ModuleInfo getModule() {
            return this.module;
        }

        @Override
        public boolean equals(@Nullable final Object that) {
            if (that == null) {
                return false;
            }
            if (that == this) {
                return true;
            }

            if (that instanceof GoExclude) {
                return this.module.equals(((GoExclude) that).module);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return this.module.hashCode();
        }
        
        @Nonnull
        @Override
        public String toString() {
            return "exclude " + this.module;
        }

        @Override
        public int getPriority() {
            return 3;
        }
    }

    public boolean hasReplaceFor(@Nonnull final String moduleName, @Nullable final String version) {
        boolean found = false;
        for (final GoModItem r : this.items) {
            if (r instanceof GoReplace) {
                final GoReplace require = (GoReplace) r;
                if (moduleName.equals(require.getModule().getName()) && (version == null || version.equals(require.getModule().getVersion()))) {
                    found = true;
                }
            }
        }
        return found;
    }

    public boolean hasRequireFor(@Nonnull final String moduleName, @Nullable final String version) {
        boolean found = false;
        for (final GoModItem r : this.items) {
            if (r instanceof GoRequire) {
                final GoRequire require = (GoRequire) r;
                if (moduleName.equals(require.getModuleInfo().getName()) && (version == null || version.equals(require.getModuleInfo().getVersion()))) {
                    found = true;
                }
            }
        }
        return found;
    }

    @Nullable
    public String getModule() {
        final List<GoModule> found = this.find(GoModule.class);
        return found.isEmpty() ? null : found.get(0).getModuleInfo().getName();
    }

    private enum ParserState {
        FIND,
        MODULE,
        REQUIRE,
        REPLACE,
        EXCLUDE,
        CUSTOM
    }

}