/*-
 * ~~~~~~licensing~~~~~~
 * atlasmap-entaxy-services
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
/*
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * 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 io.atlasmap.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.atlasmap.v2.AtlasModelFactory;
import io.atlasmap.v2.CollectionType;
import io.atlasmap.v2.Field;
import io.atlasmap.v2.FieldGroup;

public class AtlasPath implements Cloneable {

    public static final String PATH_SEPARATOR = "/";
    public static final char PATH_SEPARATOR_CHAR = '/';
    public static final String PATH_SEPARATOR_ESCAPED = "/";
    public static final String PATH_ARRAY_START = "[";
    public static final String PATH_ARRAY_END = "]";
    public static final String PATH_ARRAY_SUFFIX = PATH_ARRAY_START + PATH_ARRAY_END;
    public static final String PATH_LIST_START = "<";
    public static final String PATH_LIST_END = ">";
    public static final String PATH_LIST_SUFFIX = PATH_LIST_START + PATH_LIST_END;
    public static final String PATH_MAP_START = "{";
    public static final String PATH_MAP_END = "}";
    public static final String PATH_MAP_SUFFIX = PATH_MAP_START + PATH_MAP_END;
    public static final String PATH_ATTRIBUTE_PREFIX = "@";
    public static final String PATH_NAMESPACE_SEPARATOR = ":";

    private static final Logger LOG = LoggerFactory.getLogger(AtlasPath.class);

    protected List<SegmentContext> segmentContexts;
    private String originalPath = null;

    public AtlasPath(String p) {
        String path = p;
        this.originalPath = path;
        this.segmentContexts = parse(path);
    }

    protected AtlasPath(List<SegmentContext> segments) {
        this.segmentContexts = segments;
        this.originalPath = getSegmentPath(segments.get(segments.size() - 1));
    }

    private AtlasPath() {
        this.segmentContexts = new ArrayList<>();
    }

    /**
     * Extract child fields by feeding relative path.
     *
     * @param f Parent field to extract from
     * @param path Relative path string
     * @return extracted field(s)
     */
    public static Field extractChildren(Field f, String path) {
        if (f == null || path == null || path.isEmpty()) {
            return null;
        }

        if (path.equals(PATH_SEPARATOR)) {
            return f;
        }
        if (!(f instanceof FieldGroup)) {
            return null;
        }

        List<Field> extracted = new ArrayList<>();
        FieldGroup entryField = (FieldGroup)f;
        extracted.add(entryField);
        List<SegmentContext> entrySegments = new AtlasPath(entryField.getPath()).getSegments(true);
        SegmentContext entrySegment = entrySegments.get(entrySegments.size() - 1);
        List<SegmentContext> extractedSegments = new ArrayList<>(entrySegments);
        List<SegmentContext> relativeSegments = new AtlasPath(path).getSegments(true);
        SegmentContext relativeRootSegment = relativeSegments.get(0);

        List<Field> selected = new ArrayList<>();
        if (relativeRootSegment.getCollectionType() == null || relativeRootSegment.getCollectionType() == CollectionType.NONE) {
            if (entrySegment.getCollectionType() != null
                && entrySegment.getCollectionType() != CollectionType.NONE
                && entrySegment.getCollectionIndex() == null) {
                selected.addAll(entryField.getField());
            } else {
                selected.add(entryField);
            }
        } else if (relativeRootSegment.getCollectionIndex() != null) {
            if (entrySegment.getCollectionIndex() != null) {
                if (entrySegment.getCollectionIndex() == relativeRootSegment.getCollectionIndex()) {
                    selected.add(entryField);
                }
            } else {
                selected.add(entryField.getField().get(relativeRootSegment.getCollectionIndex()));
                entrySegment.collectionIndex = relativeRootSegment.getCollectionIndex();
                extractedSegments.set(entrySegments.size() - 1, entrySegment.rebuild());
            }
        } else {
            selected.addAll(entryField.getField());
        }
        extracted = selected;

        for (int i=1; i<relativeSegments.size(); i++) {
            SegmentContext segment = relativeSegments.get(i);
            extractedSegments.add(segment);
            selected = new ArrayList<>();

            for (Field f1 : extracted) {
                FieldGroup f1Group = (FieldGroup)f1;
                for (Field f2 : f1Group.getField()) {
                    AtlasPath f2Path = new AtlasPath(f2.getPath());
                    if (!segment.getName().equals(f2Path.getLastSegment().getName())) {
                        continue;
                    }
                    if (segment.getCollectionType() == CollectionType.NONE) {
                        selected.add(f2);
                    } else {
                        FieldGroup f2Group = (FieldGroup)f2;
                        if (segment.getCollectionIndex() != null) {
                            selected.add((f2Group.getField().get(segment.getCollectionIndex())));
                        } else {
                            selected.addAll(f2Group.getField());
                        }
                    }
                    break;
                }
            }
            extracted = selected;
        }

        if (extracted.size() == 1) {
            return extracted.get(0);
        }
        FieldGroup answer = AtlasModelFactory.createFieldGroupFrom(f, true);
        answer.setPath(new AtlasPath(extractedSegments).toString());
        answer.getField().addAll(extracted);
        return answer;
    }

    public static void setCollectionIndexRecursively(FieldGroup group, int segmentIndex, int index) {
        AtlasPath path = new AtlasPath(group.getPath());
        path.setCollectionIndex(segmentIndex, index);
        group.setPath(path.toString());
        for (Field f : group.getField()) {
            if (f instanceof FieldGroup) {
                setCollectionIndexRecursively((FieldGroup)f, segmentIndex, index);
            } else {
                AtlasPath fpath = new AtlasPath(f.getPath());
                fpath.setCollectionIndex(segmentIndex, index);
                f.setPath(fpath.toString());
            }
        }
    }

    public AtlasPath appendField(String fieldExpression) {
        this.segmentContexts.add(createSegmentContext(fieldExpression));
        return this;
    }

    @Override
    public AtlasPath clone() {
        return new AtlasPath(this.toString());
    }
    
    public List<SegmentContext> getSegments(boolean includeRoot) {
        if (includeRoot) {
            return Collections.unmodifiableList(this.segmentContexts);
        }
        if (this.segmentContexts.size() > 1) {
            return Collections.unmodifiableList(this.segmentContexts.subList(1, this.segmentContexts.size()));
        }
        return Collections.emptyList();
    }

    public Boolean isRoot() {
        return this.segmentContexts.size() == 1 && this.segmentContexts.get(0).isRoot();
    }

    public SegmentContext getRootSegment() {
        return this.segmentContexts.get(0);
    }

    public Boolean isCollectionRoot() {
        return this.segmentContexts.size() == 1 && this.segmentContexts.get(0).getCollectionType() != CollectionType.NONE;
    }

    public Boolean hasCollectionRoot() {
        return this.segmentContexts.get(0).getCollectionType() != CollectionType.NONE;
    }

    public SegmentContext getLastSegment() {
        return this.segmentContexts.get(this.segmentContexts.size()-1);
    }

    public SegmentContext getLastCollectionSegment() {
        List<SegmentContext> collectionSegments = getCollectionSegments(true);
        return collectionSegments.get(collectionSegments.size() - 1);
    }

    public SegmentContext getLastSegmentParent() {
        if (this.segmentContexts.isEmpty() || this.segmentContexts.size() == 1) {
            return null;
        }

        return this.segmentContexts.get(this.segmentContexts.size() - 2);
    }

    public AtlasPath getLastSegmentParentPath() {
        if (this.segmentContexts.isEmpty() || this.segmentContexts.size() == 1) {
            return null;
        }

        AtlasPath parentPath = new AtlasPath();
        for (int i = 0; i < this.segmentContexts.size() - 1; i++) {
            parentPath.appendField(this.segmentContexts.get(i).getExpression());
        }
        return parentPath;
    }

    public SegmentContext getParentSegmentOf(SegmentContext sc) {
        for (int i=0; i<this.segmentContexts.size(); i++) {
            if (this.segmentContexts.get(i) == sc) {
                if (sc.isRoot()) {
                    return null;
                }
                return this.segmentContexts.get(i-1);
            }
        }
        return null;
    }

    public boolean hasCollection() {
        for (SegmentContext sc : this.segmentContexts) {
            if (sc.getCollectionType() != CollectionType.NONE) {
                return true;
            }
        }
        return false;
    }

    public boolean isIndexedCollection() {
        boolean hasIndexedCollection = false;
        for (SegmentContext sc : this.segmentContexts) {
            if (sc.getCollectionType() != CollectionType.NONE) {
                if (sc.getCollectionIndex() != null) {
                    hasIndexedCollection = true;
                }

            }
        }
        return hasIndexedCollection;
    }

    public SegmentContext setCollectionIndex(int segmentIndex, Integer collectionIndex) {
        if (collectionIndex != null && collectionIndex < 0) {
            throw new IllegalArgumentException(String.format(
                    "Cannnot set negative collection index %s for the path %s",
                    collectionIndex, this.toString()));
        }
        SegmentContext sc = this.segmentContexts.get(segmentIndex);
        sc.collectionIndex = collectionIndex;
        return this.segmentContexts.set(segmentIndex, sc.rebuild());
    }

    public List<SegmentContext> getCollectionSegments(boolean includeRoot) {
        List<SegmentContext> segments = getSegments(includeRoot);
        List<SegmentContext> collectionSegments = new ArrayList<>();
        for (SegmentContext segment: segments) {
            if (segment.getCollectionType() != CollectionType.NONE) {
                collectionSegments.add(segment);
            }
        }
        return collectionSegments;
    }

    public SegmentContext setVacantCollectionIndex(Integer collectionIndex) {
        for (int i = 0; i < this.segmentContexts.size(); i++) {
            SegmentContext sc = segmentContexts.get(i);
            if (sc.getCollectionType() != CollectionType.NONE && sc.getCollectionIndex() == null) {
                return setCollectionIndex(i, collectionIndex);
            }
        }
        throw new IllegalArgumentException("No Vacant index on collection segments in the path " + this.toString());
    }

    public String getSegmentPath(SegmentContext sc) {
        int toIndex = this.segmentContexts.indexOf(sc);
        if (toIndex == -1) {
            return null;
        }
        StringBuilder builder = new StringBuilder().append(PATH_SEPARATOR_CHAR);
        if (!this.segmentContexts.get(0).getExpression().isEmpty()) {
            builder.append(this.segmentContexts.get(0).getExpression());
        }
        for (int i=1; i<=toIndex; i++) {
            if (!(builder.charAt(builder.length()-1) == PATH_SEPARATOR_CHAR)) {
                builder.append(PATH_SEPARATOR_CHAR);
            }
            builder.append(this.segmentContexts.get(i).getExpression());
        }
        return builder.toString();
    }

    @Override
    public String toString() {
        return getSegmentPath(getLastSegment());
    }

    public String getOriginalPath() {
        return originalPath;
    }

    public int getCollectionSegmentCount() {
        int answer = 0;
        for (SegmentContext sc : getSegments(true)) {
            if (sc.collectionType != null && sc.collectionType != CollectionType.NONE) {
                answer++;
            }
        }
        return answer;
    }

    protected List<SegmentContext> parse(String path) {
        path = sanitize(path);
        List<SegmentContext> segmentContexts = new ArrayList<>();
        if (path != null && !"".equals(path)) {
            if (path.startsWith(PATH_SEPARATOR)) {
                path = path.replaceFirst(PATH_SEPARATOR, "");
            }
            if (path.contains(PATH_SEPARATOR)) {
                String[] parts = path.split(PATH_SEPARATOR_ESCAPED, 512);
                for (String part : parts) {
                    segmentContexts.add(createSegmentContext(part));
                }
            } else {
                segmentContexts.add(createSegmentContext(path));
            }
        }
        if (segmentContexts.isEmpty() || !segmentContexts.get(0).isRoot()) {
            // add root segment if there's not
            segmentContexts.add(0, createSegmentContext(""));
        }
        return segmentContexts;
    }

    protected String sanitize(String path) {
        String answer = path;
        if (answer == null || answer.isEmpty()) {
            return answer;
        }
        if (answer.indexOf("//") != -1) {
            LOG.warn("Sanitizing double slash (//) in the path '{}'", answer);
            answer = answer.replaceAll("//", "/");
        }
        if (answer.endsWith("/")) {
            LOG.warn("Sanitizing trailing slash (/) in the path '{}'", answer);
            answer = answer.substring(0, answer.length()-1);

        }
        return answer;
    }

    protected SegmentContext createSegmentContext(String expression) {
        return new SegmentContext(expression);
    }

    public static class SegmentContext {

        private String name;
        private String expression;
        private CollectionType collectionType;
        private Integer collectionIndex;
        private String mapKey;
        private boolean isAttribute;
        private boolean isRoot;

        public SegmentContext(String expression) {
            this.expression = expression;
            if (this.expression.startsWith(PATH_SEPARATOR)) {
                this.expression = this.expression.replaceFirst(PATH_SEPARATOR, "");
            }
            this.name = cleanPathSegment(expression);
            if (expression.contains(PATH_MAP_START)) {
                this.collectionType = CollectionType.MAP;
            } else if (expression.contains(PATH_ARRAY_START)) {
                this.collectionType = CollectionType.ARRAY;
            } else if (expression.contains(PATH_LIST_START)) {
                this.collectionType = CollectionType.LIST;
            } else {
                this.collectionType = CollectionType.NONE;
            }
            if (this.collectionType == CollectionType.MAP) {
                this.mapKey = getMapKey(expression);
            } else {
                this.collectionIndex = getCollectionIndex(expression);
            }
            this.isAttribute = expression.startsWith(PATH_ATTRIBUTE_PREFIX);
            this.isRoot = this.name.isEmpty();
        }

        public String getName() {
            return name;
        }

        public String getExpression() {
            return expression;
        }

        public CollectionType getCollectionType() {
            return this.collectionType;
        }

        public Integer getCollectionIndex() {
            return this.collectionIndex;
        }

        public String getMapKey() {
            return this.mapKey;
        }

        public boolean isAttribute() {
            return isAttribute;
        }

        public boolean isRoot() {
            return isRoot;
        }

        protected SegmentContext rebuild() {
            StringBuilder buf = new StringBuilder();
            if (this.isAttribute) {
                buf.append(PATH_ATTRIBUTE_PREFIX);
            }
            buf.append(name);
            String index = collectionIndex != null ? collectionIndex.toString() : "";
            if (this.collectionType == CollectionType.ARRAY) {
                buf.append(PATH_ARRAY_START).append(index).append(PATH_ARRAY_END);
            } else if (this.collectionType == CollectionType.LIST) {
                buf.append(PATH_LIST_START).append(index).append(PATH_LIST_END);
            } else if (this.collectionType == CollectionType.MAP) {
                buf.append(PATH_LIST_START).append(mapKey).append(PATH_LIST_END);
            }
            return new SegmentContext(buf.toString());
        }

        @Override
        public String toString() {
            return collectionType == CollectionType.MAP
                ? String.format("SegmentContext [name=%s, expression=%s, collectionType=%s, mapKey=%s]",
                    name, expression, collectionType, mapKey)
                : String.format(
                    "SegmentContext [name=%s, expression=%s, collectionType=%s, collectionIndex=%s]",
                    name, expression, collectionType, collectionIndex);
        }

        protected String cleanPathSegment(String expression) {
            String answer = expression;
            if (answer == null) {
                return null;
            }

            // strip namespace if there is one
            if (answer.contains(PATH_NAMESPACE_SEPARATOR)) {
                answer = answer.substring(answer.indexOf(PATH_NAMESPACE_SEPARATOR) + 1);
            }

            if (answer.contains(PATH_ARRAY_START) && answer.endsWith(PATH_ARRAY_END)) {
                return answer.substring(0, answer.indexOf(PATH_ARRAY_START, 0));
            }

            if (answer.contains(PATH_LIST_START) && answer.endsWith(PATH_LIST_END)) {
                return answer.substring(0, answer.indexOf(PATH_LIST_START, 0));
            }

            if (answer.contains(PATH_MAP_START) && answer.endsWith(PATH_MAP_END)) {
                return answer.substring(0, answer.indexOf(PATH_MAP_START, 0));
            }

            return answer;
        }

        private Integer getCollectionIndex(String expression) {
            if (expression == null) {
                return null;
            }

            if (expression.contains(PATH_ARRAY_START) && expression.endsWith(PATH_ARRAY_END)) {
                int start = expression.indexOf(PATH_ARRAY_START, 0) + 1;
                String index = expression.substring(start, expression.indexOf(PATH_ARRAY_END, start));
                if (index != null && index.length() > 0) {
                    return Integer.valueOf(index);
                }
                return null;
            }

            if (expression.contains(PATH_LIST_START) && expression.endsWith(PATH_LIST_END)) {
                int start = expression.indexOf(PATH_LIST_START, 0) + 1;
                String index = expression.substring(start, expression.indexOf(PATH_LIST_END, start));
                if (index != null && index.length() > 0) {
                    return Integer.valueOf(index);
                }
                return null;
            }

            return null;
        }

        private String getMapKey(String expression) {
            int start = expression.indexOf(PATH_MAP_START, 0) + 1;
            String key = expression.substring(start, expression.indexOf(PATH_MAP_END, start));
            if (key != null && key.length() > 0) {
                return key;
            }
            return null;
        }
    }

}
