/**
 *
 */
package com.thinkbiganalytics.metadata.modeshape.support;

/*-
 * #%L
 * thinkbig-metadata-modeshape
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * 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.
 * #L%
 */

import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchEvent.Modifier;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Really just a generic path implementation but currently being used in the JCR utilities.
 */
public class JcrPath implements Path {

    private final List<String> elements;
    private final boolean absolute;

    public JcrPath(boolean absolute, List<String> elements) {
        this.elements = Collections.unmodifiableList(elements);
        this.absolute = absolute;
    }

    public JcrPath(boolean absolute, String element) {
        this.elements = Collections.singletonList(element);
        this.absolute = absolute;
    }

    public static Path get(String first, String... more) {
        ArrayList<String> list = new ArrayList<>();
        boolean absolute = first.trim().startsWith("/");
        String[] firstElems = first.split("/");

        for (int idx = (absolute ? 1 : 0); idx < firstElems.length; idx++) {
            if (StringUtils.isNotBlank(firstElems[idx])) {
                list.add(firstElems[idx]);
            }
        }

        for (String another : more) {
            String[] anotherElems = another.split("/");

            for (String elem : anotherElems) {
                if (StringUtils.isNotBlank(elem)) {
                    list.add(elem);
                }
            }
        }

        return new JcrPath(absolute, list);
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#getFileSystem()
     */
    @Override
    public FileSystem getFileSystem() {
        return null;
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#isAbsolute()
     */
    @Override
    public boolean isAbsolute() {
        return this.absolute;
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#getRoot()
     */
    @Override
    public Path getRoot() {
        return this.absolute ? new JcrPath(true, Collections.emptyList()) : null;
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#getFileName()
     */
    @Override
    public Path getFileName() {
        return this.elements.size() > 0 ? new JcrPath(false, this.elements.get(this.elements.size() - 1)) : null;
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#getParent()
     */
    @Override
    public Path getParent() {
        if (this.elements.size() > 1) {
            return new JcrPath(this.absolute, this.elements.subList(0, this.elements.size() - 2));
        } else {
            return null;
        }
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#getNameCount()
     */
    @Override
    public int getNameCount() {
        return this.elements.size();
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#getName(int)
     */
    @Override
    public Path getName(int index) {
        if (index < 0 || index > this.elements.size() - 1) {
            throw new IllegalArgumentException("Index is out of bounds: " + index);
        } else if (index == 0) {
            return new JcrPath(this.isAbsolute(), this.elements.get(0));
        } else {
            return new JcrPath(false, this.elements.get(index));
        }
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#subpath(int, int)
     */
    @Override
    public Path subpath(int beginIndex, int endIndex) {
        if (beginIndex < 0 || beginIndex > this.elements.size() - 1 || endIndex < beginIndex || endIndex > this.elements.size()) {
            throw new IllegalArgumentException("Invalid indices bounds: " + beginIndex + ", " + endIndex);
        } else {
            return new JcrPath(this.isAbsolute() && beginIndex == 0, this.elements.subList(beginIndex, endIndex));
        }
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#startsWith(java.nio.file.Path)
     */
    @Override
    public boolean startsWith(Path other) {
        if (other.isAbsolute() != isAbsolute()) {
            return false;
        } else if (other.getNameCount() > getNameCount()) {
            return false;
        } else {
            for (int idx = 0; idx < other.getNameCount(); idx++) {
                Path otherElem = other.getName(idx);

                if (otherElem.getFileName().equals(this.elements.get(idx))) {
                    return false;
                }
            }

            return true;
        }
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#startsWith(java.lang.String)
     */
    @Override
    public boolean startsWith(String other) {
        Path otherPath = get(other);
        return startsWith(otherPath);
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#endsWith(java.nio.file.Path)
     */
    @Override
    public boolean endsWith(Path other) {
        int offset = getNameCount() - other.getNameCount();

        if (offset < 0) {
            return false;
        } else {
            for (int idx = getNameCount() - 1; idx - offset >= 0; idx--) {
                Path otherElem = other.getName(idx - offset);

                if (otherElem.getFileName().equals(this.elements.get(idx))) {
                    return false;
                }
            }

            return true;
        }
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#endsWith(java.lang.String)
     */
    @Override
    public boolean endsWith(String other) {
        Path otherPath = get(other);
        return startsWith(otherPath);
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#normalize()
     */
    @Override
    public Path normalize() {
        return this;
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#resolve(java.nio.file.Path)
     */
    @Override
    public Path resolve(Path other) {
        if (other.isAbsolute()) {
            return other;
        } else if (other.getNameCount() == 0) {
            return this;
        } else {
            List<String> elements = new ArrayList<>(this.elements);
            for (int idx = 0; idx < other.getNameCount(); idx++) {
                elements.add(other.getName(idx).toString());
            }
            return new JcrPath(this.absolute, elements);
        }
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#resolve(java.lang.String)
     */
    @Override
    public Path resolve(String other) {
        Path otherPath = get(other);
        return resolve(otherPath);
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#resolveSibling(java.nio.file.Path)
     */
    @Override
    public Path resolveSibling(Path other) {
        if (other.isAbsolute()) {
            return other;
        } else if (other.getNameCount() == 0) {
            return this;
        } else {
            return getParent().resolve(other);
        }
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#resolveSibling(java.lang.String)
     */
    @Override
    public Path resolveSibling(String other) {
        Path otherPath = get(other);
        return resolveSibling(otherPath);
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#relativize(java.nio.file.Path)
     */
    @Override
    public Path relativize(Path other) {
        if (other.isAbsolute() != isAbsolute() || other.getNameCount() < getNameCount()) {
            return other;
        } else if (other.getNameCount() == 0) {
            return this;
        } else {
            int idx = 0;
            for (; idx < getNameCount(); idx++) {
                if (!other.getName(idx).equals(getName(idx))) {
                    return other;
                }
            }
            return other.subpath(idx - 1, other.getNameCount());
        }
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#toUri()
     */
    @Override
    public URI toUri() {
        return URI.create(toString());
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#toAbsolutePath()
     */
    @Override
    public Path toAbsolutePath() {
        // TODO Support a root node?
        return get("/").resolve(this);
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#toRealPath(java.nio.file.LinkOption[])
     */
    @Override
    public Path toRealPath(LinkOption... options) throws IOException {
        return this;
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#toFile()
     */
    @Override
    public File toFile() {
        return new File(toUri());
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#register(java.nio.file.WatchService, java.nio.file.WatchEvent.Kind[], java.nio.file.WatchEvent.Modifier[])
     */
    @Override
    public WatchKey register(WatchService watcher, Kind<?>[] events, Modifier... modifiers) throws IOException {
        throw new UnsupportedOperationException("Register not supported");
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#register(java.nio.file.WatchService, java.nio.file.WatchEvent.Kind[])
     */
    @Override
    public WatchKey register(WatchService watcher, Kind<?>... events) throws IOException {
        throw new UnsupportedOperationException("Register not supported");
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#iterator()
     */
    @Override
    public Iterator<Path> iterator() {
        return this.elements.stream().map(e -> new JcrPath(false, e)).collect(Collectors.<Path>toList()).iterator();
    }

    /* (non-Javadoc)
     * @see java.nio.file.Path#compareTo(java.nio.file.Path)
     */
    @Override
    public int compareTo(Path other) {
        int len = Math.min(other.getNameCount(), getNameCount());

        for (int idx = 0; idx < len; idx++) {
            int value = other.getName(idx).toString().compareTo(getName(idx).toString());
            if (value != 0) {
                return value;
            }
        }

        return Integer.compare(other.getNameCount(), getNameCount());
    }

    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Path) {
            Path other = (Path) obj;
            if (isAbsolute() == other.isAbsolute()) {
                return compareTo(other) == 0;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return Boolean.hashCode(this.absolute)
               + this.elements.stream()
                   .mapToInt(String::hashCode)
                   .reduce((h, e) -> h + e)
                   .orElse(0);
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        StringBuilder bldr = new StringBuilder();
        if (this.absolute) {
            bldr.append("/");
        }
        if (this.elements.size() > 0) {
            bldr.append(this.elements.get(0));
        }

        for (int idx = 1; idx < this.elements.size(); idx++) {
            bldr.append("/").append(this.elements.get(idx));
        }

        return bldr.toString();
    }
}
