/*
 * Copyright (c) 2016 MRV Communications, Inc. All rights reserved.
 *  This program and the accompanying materials are made available under the
 *  terms of the Eclipse Public License v1.0 which accompanies this distribution,
 *  and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 *  Contributors:
 *      Christopher Murch <[email protected]>
 *      Bartosz Michalik <[email protected]>
 */

package com.mrv.yangtools.codegen;

import com.mrv.yangtools.codegen.impl.TypeConverter;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.parameters.PathParameter;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Helper class that help to keep track of current location in YANG module data tree.
 * Segment stores current node related data and can point to its parent that
 * effectively allows for path computation from current node to root node.
 * @author [email protected]
 * @author [email protected]
 */
public class PathSegment implements Iterable<PathSegment> {
    private static final Logger log = LoggerFactory.getLogger(PathSegment.class);

    private String name;
    private String moduleName;
    private PathSegment parent;
    private ListSchemaNode node;
    private TypeConverter converter;

    //local parameters
    private List<Parameter> localParams;
    private boolean readOnly;

    /**
     * To create a root segment of path
     * @param ctx YANG context
     */
    public PathSegment(SchemaContext ctx) {
        this(NULL);
        this.converter = new TypeConverter(ctx) {
            @Override
            protected boolean enumToModel() {
                return false;
            }
        };
    }

    private PathSegment() {}

    public PathSegment(PathSegment parent) {
        Objects.requireNonNull(parent);
        this.parent = parent;
        this.converter = parent.converter;
        this.moduleName = parent.moduleName;
        this.readOnly = parent.readOnly;
        node = null;
    }

    public PathSegment withName(String name) {
        log.debug("adding {} to {}", name, parent.name);
        this.name = name;
        return this;
    }

    public PathSegment withModule(String module) {
        this.moduleName = module;
        return this;
    }

    public PathSegment withListNode(ListSchemaNode node) {
        this.node = node;
        return this;
    }

    public PathSegment asReadOnly(boolean readOnly) {
        if(!parent.readOnly) {
            this.readOnly = readOnly;
        } else {
            // https://tools.ietf.org/html/rfc6020#section-7.19.1
            log.debug("parent {} is read-only ignoring current flag", parent.name);
        }
        return this;
    }

    public String getName() { return name;}
    public String getModuleName() { return moduleName;}
    public Collection<? extends Parameter> getParam() { return localParameters();}

    public boolean forList() {
        return node != null && !node.getKeyDefinition().isEmpty();
    }

    public boolean isReadOnly() {
        return readOnly;
    }

    public PathSegment drop() {
        log.debug("dropping {} segment", name);
        return parent();
    }

    public PathSegment parent() {
        return this.parent;
    }

    @Override
    public String toString() {
        return "PathSegment{" +
                "name='" + name + '\'' +
                ", moduleName='" + moduleName + '\'' +
                ", parent=" + parent +
                ", node=" + node +
                ", converter=" + converter +
                ", localParams=" + localParams +
                ", readOnly=" + readOnly +
                '}';
    }


    public List<Parameter> params() {
        final List<Parameter> params = parent.params();
        params.addAll(localParameters());

        return params;
    }

    public List<Parameter> listParams() {
        return parent.params();
    }

    protected Collection<? extends Parameter> localParameters() {
        if(localParams == null) {
            if(node != null) {
                log.debug("processing parameters from attached node");
                final Set<String> existingNames = parent.params().stream().map(Parameter::getName).collect(Collectors.toSet());

                localParams = node.getKeyDefinition().stream()
                        .map(k -> {

                            final String name = generateName(k, existingNames);

                            final PathParameter param = new PathParameter()
                                    .name(name);

                            final Optional<LeafSchemaNode> keyNode = node.getChildNodes().stream()
                                    .filter(n -> n instanceof LeafSchemaNode)
                                    .filter(n -> n.getQName().equals(k))
                                    .map(n -> ((LeafSchemaNode)n))
                                    .findFirst();

                            if(keyNode.isPresent()) {
                                final LeafSchemaNode kN = keyNode.get();
                                param
                                    .description("Id of " + node.getQName().getLocalName())
                                    .property(converter.convert(kN.getType(), kN));
                            }

                            return param;
                        })
                        .collect(Collectors.toList());
            } else {
                localParams = Collections.emptyList();
            }
        }

        return localParams;
    }

    protected String generateName(QName paramName, Set<String> existingNames) {
        String name = paramName.getLocalName();
        if(! existingNames.contains(name)) return name;
        name = this.name + "-" + name;

        if(! existingNames.contains(name)) return name;

        name = moduleName + "-" + name;

        if(! existingNames.contains(name)) return name;

        //brute-force
        final String tmpName = name;

        return IntStream.range(1, 102)
                .mapToObj(i -> tmpName + i)
                .filter(n -> !existingNames.contains(n))
                .findFirst().orElseThrow(IllegalStateException::new);
    }

    @Override
    public Iterator<PathSegment> iterator() {
        return new Iterator<PathSegment>() {

            private PathSegment current = PathSegment.this;

            @Override
            public boolean hasNext() {
                return current != NULL;
            }

            @Override
            public PathSegment next() {
                PathSegment r = current;
                current = current.parent;
                return r;
            }
        };
    }

    public Stream<PathSegment> stream() {
        return StreamSupport.stream(this.spliterator(), false);
    }

    private static PathSegment NULL = new PathSegment() {

        @Override
        public PathSegment drop() {
            return null;
        }

        @Override
        public List<Parameter> params() {
            return new ArrayList<>();
        }

        @Override
        public List<Parameter> listParams() {
            return params();
        }
    };
}