/*
 * Copyright 2019 WeBank
 *
 * 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.webank.wedatasphere.linkis.common.io;

import org.apache.commons.lang.StringUtils;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Created by enjoyyin on 2017/3/13.
 */
public class FsPath {

    public static final String CUR_DIR = ".";
    public static final boolean WINDOWS
            = System.getProperty("os.name").startsWith("Windows");
    public static final char SEPARATOR_CHAR = '/';
    public static final String SEPARATOR = "/";
    private static final Pattern hasDriveLetterSpecifier = Pattern.compile("^/?[a-zA-Z]:");

    private URI uri;
    private long modification_time;
    private long access_time;
    private long length;
    private boolean isdir;
    private String permissionString;
    private String owner;
    private String group;
    private PosixFilePermission[] permissions;

    public FsPath(String pathString) throws IllegalArgumentException {
        checkPathArg(pathString);
        // We can't use 'new URI(String)' directly, since it assumes things are
        // escaped, which we don't require of Paths.

        // add a slash in front of paths with Windows drive letters
        if (hasWindowsDrive(pathString) && pathString.charAt(0) != '/') {
            pathString = "/" + pathString;
        }
        // parse uri components
        String scheme = null;
        String authority = null;

        int start = 0;
        // parse uri scheme, if any
        int colon = pathString.indexOf(':');
        int slash = pathString.indexOf('/');
        if ((colon != -1) &&
                ((slash == -1) || (colon < slash))) {     // has a scheme
            scheme = pathString.substring(0, colon);
            start = colon+1;
        }
        // parse uri authority, if any
        if (pathString.startsWith("//", start) &&
                (pathString.length()-start > 2)) {       // has authority
            int nextSlash = pathString.indexOf('/', start+2);
            int authEnd = nextSlash > 0 ? nextSlash : pathString.length();
            authority = pathString.substring(start+2, authEnd);
            start = authEnd;
        }

        // uri path is the rest of the string -- query & fragment not supported
        String path = pathString.substring(start, pathString.length());
        initialize(scheme, authority, path, null);
    }

    public FsPath(String scheme, String authority, String path) {
        checkPathArg(path);
        // add a slash in front of paths with Windows drive letters
        if (hasWindowsDrive(path) && path.charAt(0) != '/') {
            path = "/" + path;
        }
        // add "./" in front of Linux relative paths so that a path containing
        // a colon e.q. "a:b" will not be interpreted as scheme "a".
        if (!WINDOWS && path.charAt(0) != '/') {
            path = "./" + path;
        }
        initialize(scheme, authority, path, null);
    }

    private void checkPathArg( String path ) throws IllegalArgumentException {
        // disallow construction of a Path from an empty string
        if ( path == null ) {
            throw new IllegalArgumentException(
                    "Can not create a Path from a null string");
        }
        if( path.length() == 0 ) {
            throw new IllegalArgumentException(
                    "Can not create a Path from an empty string");
        }
    }

    private void initialize(String scheme, String authority, String path,
                            String fragment) {
        try {
            this.uri = new URI(scheme, authority, normalizePath(scheme, path), null, fragment)
                    .normalize();
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private static String normalizePath(String scheme, String path) {
        // Remove double forward slashes.
        path = StringUtils.replace(path, "//", "/");

        // Remove backslashes if this looks like a Windows path. Avoid
        // the substitution if it looks like a non-local URI.
        if (WINDOWS &&
                (hasWindowsDrive(path) ||
                        (scheme == null) ||
                        (scheme.isEmpty()) ||
                        (scheme.equals("file")))) {
            path = StringUtils.replace(path, "\\", "/");
        }

        // trim trailing slash from non-root path (ignoring windows drive)
        int minLength = startPositionWithoutWindowsDrive(path) + 1;
        if (path.length() > minLength && path.endsWith(SEPARATOR)) {
            path = path.substring(0, path.length()-1);
        }

        return path;
    }

    private static boolean hasWindowsDrive(String path) {
        return (WINDOWS && hasDriveLetterSpecifier.matcher(path).find());
    }

    private static int startPositionWithoutWindowsDrive(String path) {
        if (hasWindowsDrive(path)) {
            return path.charAt(0) ==  SEPARATOR_CHAR ? 3 : 2;
        } else {
            return 0;
        }
    }

    /**
     *
     * @return if no storage type pointed, hdfs type will returned
     */
    public String getFsType() {
        return getFsType("file");
    }

    public String getFsType(String defaultType) {
        String scheme = uri.getScheme();
        if(StringUtils.isEmpty(scheme)) {
            return defaultType;
        } else {
            return scheme;
        }
    }

    public File toFile() {
        return new File(uri);
    }

    public Path toPath() {
        return FileSystems.getDefault().getPath(uri.toString());
    }

    public boolean isOwner(String user) {
        return owner.equals(user);
    }

    public FsPath getParent() throws IOException {
        String path = uri.getPath();
        int lastSlash = path.lastIndexOf('/');
        int start = startPositionWithoutWindowsDrive(path);
        if ((path.length() == start) ||               // empty path
                (lastSlash == start && path.length() == start + 1)) { // at root
            return null;
        }
        String parent;
        if (lastSlash == -1) {
            parent = CUR_DIR;
        } else {
            parent = path.substring(0, lastSlash == start ? start + 1 : lastSlash);
        }
        return new FsPath(uri.getScheme(), uri.getAuthority(), parent);
    }

    public long getModification_time() {
        return modification_time;
    }

    public void setModification_time(long modification_time) {
        this.modification_time = modification_time;
    }

    public long getAccess_time() {
        return access_time;
    }

    public void setAccess_time(long access_time) {
        this.access_time = access_time;
    }

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

    public boolean isdir() {
        return isdir;
    }

    public void setIsdir(boolean isdir) {
        this.isdir = isdir;
    }

    public String getPermissionString() {
        return permissionString;
    }

    public String getOwner() {
        return owner;
    }

    public void setOwner(String owner) {
        this.owner = owner;
    }

    public String getGroup() {
        return group;
    }

    public void setGroup(String group) {
        this.group = group;
    }

    public void setPermissions(Set<PosixFilePermission> permissions) throws IOException {
//        this.permissions = FileSystem.permissionFormatted(permissions);
        this.permissions = permissions.toArray(new PosixFilePermission[] {});
        this.permissionString = PosixFilePermissions.toString(permissions);
    }

    public void setPermissionString(String permissionString) throws IOException {
        this.permissionString = permissionFormatted(permissionString);
        this.permissions = PosixFilePermissions.fromString(this.permissionString).toArray(new PosixFilePermission[] {});
    }

    public String getPath() {
        if(WINDOWS && ! "hdfs".equals(getFsType())){
            return uri.getAuthority() + uri.getPath();
        }
        return uri.getPath();
    }

    public String getSchemaPath(){
        if(WINDOWS && ! "hdfs".equals(getFsType())){
            return getFsType() + "://" + uri.getAuthority() + uri.getPath();
        }
        return getFsType() + "://" + uri.getPath();
    }
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append("{");
        sb.append("path=").append(uri.getPath());
        sb.append("; isDirectory=").append(isdir);
        if(!isdir){
            sb.append("; length=").append(length);
        }
        sb.append("; modification_time=").append(modification_time);
        sb.append("; access_time=").append(access_time);
        sb.append("; owner=").append(owner);
        sb.append("; group=").append(group);
        sb.append("; permission=").append(permissions);
        sb.append("}");
        return sb.toString();
    }

    public static String permissionFormatted(String permission) throws IOException {
        if(!StringUtils.isNumeric(permission)) {
            return permission;
        }
        char[] ps = permission.toCharArray();
        return permissionFormatted(ps[0]) + permissionFormatted(ps[1]) + permissionFormatted(ps[2]);
    }

    public static String permissionFormatted(char i) throws IOException {
        int in = Integer.parseInt(String.valueOf(i));
        switch (in) {
            case 0: return "---";
            case 1: return "--x";
            case 2: return "-w-";
            case 3: return "-wx";
            case 4: return "r--";
            case 5: return "r-x";
            case 6: return "rw-";
            case 7: return "rwx";
            default: throw new IOException("Incorrent permission number " + in);
        }
    }

    public static FsPath getFsPath(String path, String... more) {
        String realPath = Paths.get(path, more).toFile().getPath();
        return new FsPath(realPath);
    }
}
