/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.uberfire.io.impl;

import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.uberfire.io.IOService;
import org.uberfire.io.IOWatchService;
import org.uberfire.java.nio.IOException;
import org.uberfire.java.nio.base.AbstractBasicFileAttributeView;
import org.uberfire.java.nio.base.AttrHolder;
import org.uberfire.java.nio.base.NeedsPreloadedAttrs;
import org.uberfire.java.nio.base.Properties;
import org.uberfire.java.nio.base.dotfiles.DotFileOption;
import org.uberfire.java.nio.channels.SeekableByteChannel;
import org.uberfire.java.nio.file.AtomicMoveNotSupportedException;
import org.uberfire.java.nio.file.CopyOption;
import org.uberfire.java.nio.file.DeleteOption;
import org.uberfire.java.nio.file.DirectoryNotEmptyException;
import org.uberfire.java.nio.file.FileAlreadyExistsException;
import org.uberfire.java.nio.file.Files;
import org.uberfire.java.nio.file.NoSuchFileException;
import org.uberfire.java.nio.file.OpenOption;
import org.uberfire.java.nio.file.Path;
import org.uberfire.java.nio.file.attribute.FileAttribute;
import org.uberfire.java.nio.file.attribute.FileAttributeView;

import static org.kie.soup.commons.validation.PortablePreconditions.checkNotEmpty;
import static org.kie.soup.commons.validation.PortablePreconditions.checkNotNull;
import static org.uberfire.java.nio.base.dotfiles.DotFileUtils.buildDotFile;
import static org.uberfire.java.nio.base.dotfiles.DotFileUtils.consolidate;
import static org.uberfire.java.nio.base.dotfiles.DotFileUtils.dot;
import static org.uberfire.java.nio.file.StandardCopyOption.REPLACE_EXISTING;

public class IOServiceDotFileImpl
        extends AbstractIOService
        implements IOService {

    public IOServiceDotFileImpl() {
        super();
    }

    public IOServiceDotFileImpl(final String serviceId) {
        super(serviceId);
    }

    public IOServiceDotFileImpl(final IOWatchService watchService) {
        super(watchService);
    }

    public IOServiceDotFileImpl(final String serviceId,
                                final IOWatchService watchService) {
        super(serviceId,
              watchService);
    }

    @Override
    public void delete(final Path path,
                       final DeleteOption... options)
            throws IllegalArgumentException, NoSuchFileException, DirectoryNotEmptyException,
            IOException, SecurityException {
        Files.delete(path,
                     options);
        try {
            Files.deleteIfExists(dot(path),
                                 options);
        } catch (Exception ex) {
        }
        if (path instanceof AttrHolder) {
            ((AttrHolder) path).getAttrStorage().clear();
        }
    }

    @Override
    public boolean deleteIfExists(final Path path,
                                  final DeleteOption... options)
            throws IllegalArgumentException, DirectoryNotEmptyException, IOException, SecurityException {
        final boolean result = Files.deleteIfExists(path,
                                                    options);
        try {
            Files.deleteIfExists(dot(path),
                                 options);
        } catch (Exception ex) {
        }
        if (path instanceof AttrHolder) {
            ((AttrHolder) path).getAttrStorage().clear();
        }
        return result;
    }

    @Override
    public SeekableByteChannel newByteChannel(final Path path,
                                              final Set<? extends OpenOption> options,
                                              final FileAttribute<?>... attrs)
            throws IllegalArgumentException, UnsupportedOperationException,
            FileAlreadyExistsException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        final Properties properties = new Properties();
        if (exists(dot(path))) {
            properties.load(newInputStream(dot(path)));
        }
        final FileAttribute<?>[] allAttrs = consolidate(properties,
                                                        attrs);

        final SeekableByteChannel result = Files.newByteChannel(path,
                                                                buildOptions(options),
                                                                allAttrs);

        if (isFileScheme(path)) {
            buildDotFile(path,
                         newOutputStream(dot(path)),
                         allAttrs);
        }

        return result;
    }

    @Override
    public Path createDirectory(final Path dir,
                                final FileAttribute<?>... attrs)
            throws IllegalArgumentException, UnsupportedOperationException, FileAlreadyExistsException,
            IOException, SecurityException {
        return internalCreateDirectory(dir,
                                       false,
                                       attrs);
    }

    @Override
    public Path createDirectories(final Path dir,
                                  final FileAttribute<?>... attrs)
            throws UnsupportedOperationException, FileAlreadyExistsException,
            IOException, SecurityException {
        final Path result = Files.createDirectories(dir,
                                                    attrs);

        buildDotFile(dir,
                     newOutputStream(dot(dir)),
                     attrs);

        return result;
    }

    @Override
    public Path copy(final Path source,
                     final Path target,
                     final CopyOption... options)
            throws UnsupportedOperationException, FileAlreadyExistsException,
            DirectoryNotEmptyException, IOException, SecurityException {
        if (Files.exists(dot(source))) {
            Files.copy(dot(source),
                       dot(target),
                       forceBuildOptions(options));
        } else if (Files.exists(dot(target))) {
            Files.delete(dot(target));
        }

        final Path result = Files.copy(source,
                                       target,
                                       options);

        return result;
    }

    @Override
    public Path move(final Path source,
                     final Path target,
                     final CopyOption... options)
            throws UnsupportedOperationException, FileAlreadyExistsException,
            DirectoryNotEmptyException, AtomicMoveNotSupportedException, IOException, SecurityException {
        if (Files.exists(dot(source))) {
            Files.move(dot(source),
                       dot(target),
                       forceBuildOptions(options));
        } else if (Files.exists(dot(target))) {
            Files.delete(dot(target));
        }

        final Path result = Files.move(source,
                                       target,
                                       options);

        return result;
    }

    @Override
    public <V extends FileAttributeView> V getFileAttributeView(final Path path,
                                                                final Class<V> type)
            throws IllegalArgumentException {

        final V value = Files.getFileAttributeView(path,
                                                   type);

        if (value == null && path instanceof AttrHolder) {
            final AttrHolder holder = ((AttrHolder) path);
            final V holderView = holder.getAttrView(type);
            if (holderView == null && AbstractBasicFileAttributeView.class.isAssignableFrom(type)) {
                return (V) newView(holder,
                                   (Class<? extends AbstractBasicFileAttributeView>) type);
            }
            return holderView;
        }

        return value;
    }

    @Override
    public Map<String, Object> readAttributes(final Path path,
                                              final String attributes)
            throws UnsupportedOperationException, NoSuchFileException, IllegalArgumentException,
            IOException, SecurityException {
        checkNotNull("path",
                     path);
        checkNotEmpty("attributes",
                      attributes);

        final Properties original = new Properties(Files.readAttributes(path,
                                                                        attributes));
        if (attributes.equals("*") && exists(dot(path))) {
            boolean isAttrHolder = path instanceof AttrHolder;
            if (isAttrHolder && ((AttrHolder) path).getAttrStorage().getContent().size() > 0) {
                return ((AttrHolder) path).getAttrStorage().getAllContent();
            }
            final Properties content = new Properties();
            content.load(newInputStream(dot(path)));
            content.putAll(original);

            if (isAttrHolder) {
                ((AttrHolder) path).getAttrStorage().loadContent(content);
            }
            return content;
        }

        return original;
    }

    @Override
    public Path setAttributes(final Path path,
                              final FileAttribute<?>... attrs)
            throws UnsupportedOperationException, IllegalArgumentException, ClassCastException, IOException, SecurityException {
        checkNotNull("path",
                     path);
        if (Files.isDirectory(path)) {
            return internalCreateDirectory(path,
                                           true,
                                           attrs);
        }
        return write(path,
                     readAllBytes(path),
                     Collections.<OpenOption>emptySet(),
                     attrs);
    }

    @Override
    public Object getAttribute(final Path path,
                               final String attribute)
            throws UnsupportedOperationException, IllegalArgumentException, IOException, SecurityException {
        checkNotNull("path",
                     path);

        Object value;
        try {
            value = Files.getAttribute(path,
                                       attribute);
        } catch (UnsupportedOperationException ex) {
            value = null;
        }

        if (value == null && path instanceof AttrHolder) {
            final AttrHolder holder = ((AttrHolder) path);
            final String[] attr = split(attribute);
            if (holder.getAttrStorage().getContent().isEmpty()) {
                loadDotFile(path);
            }
            return holder.getAttrStorage().getAllContent().get(attr[1]);
        }

        return value;
    }

    @Override
    protected Set<? extends OpenOption> buildOptions(final Set<? extends OpenOption> options,
                                                     final OpenOption... others) {
        return new HashSet<OpenOption>(options) {{
            add(new DotFileOption());
            if (others != null) {
                for (final OpenOption other : others) {
                    add(other);
                }
            }
        }};
    }

    protected CopyOption[] buildOptions(final CopyOption... options) {
        final CopyOption[] result = new CopyOption[options.length + 1];
        System.arraycopy(options,
                         0,
                         result,
                         0,
                         options.length);
        result[result.length - 1] = new DotFileOption();
        return result;
    }

    protected CopyOption[] forceBuildOptions(final CopyOption[] options) {
        final CopyOption[] result = new CopyOption[options.length + 1];
        System.arraycopy(options,
                         0,
                         result,
                         0,
                         options.length);
        result[result.length - 1] = REPLACE_EXISTING;
        return result;
    }

    protected boolean isFileScheme(final Path path) {
        if (path == null || path.getFileSystem() == null || path.getFileSystem().provider() == null) {
            return false;
        }

        return path.getFileSystem().provider().getScheme().equals("file");
    }

    protected void loadDotFile(final Path path) {
        final Properties content = new Properties();
        content.load(newInputStream(dot(path)));

        if (path instanceof AttrHolder) {
            ((AttrHolder) path).getAttrStorage().loadContent(content);
        }
    }

    protected <V extends AbstractBasicFileAttributeView> V newView(final AttrHolder holder,
                                                                   final Class<V> type) {
        if (NeedsPreloadedAttrs.class.isAssignableFrom(type) && holder.getAttrStorage().getContent().size() == 0) {
            readAttributes((Path) holder);
        }

        try {
            final Constructor<V> constructor = (Constructor<V>) type.getConstructors()[0];
            final V view = constructor.newInstance(holder);
            holder.addAttrView(view);
            return view;
        } catch (final Exception e) {
        }
        return null;
    }

    protected Path internalCreateDirectory(final Path dir,
                                           final boolean skipAlreadyExistsException,
                                           final FileAttribute<?>... attrs)
            throws IllegalArgumentException, UnsupportedOperationException, FileAlreadyExistsException,
            IOException, SecurityException {
        checkNotNull("dir",
                     dir);
        FileAttribute<?>[] allAttrs = attrs;
        try {
            Files.createDirectory(dir,
                                  attrs);
        } catch (final FileAlreadyExistsException ex) {
            final Properties properties = new Properties();
            if (exists(dot(dir))) {
                properties.load(newInputStream(dot(dir)));
            }
            allAttrs = consolidate(properties,
                                   attrs);
            if (!skipAlreadyExistsException) {
                throw ex;
            }
        }

        buildDotFile(dir,
                     newOutputStream(dot(dir)),
                     allAttrs);

        return dir;
    }

    protected String[] split(final String attribute) {
        final String[] s = new String[2];
        final int pos = attribute.indexOf(':');
        if (pos == -1) {
            s[0] = "basic";
            s[1] = attribute;
        } else {
            s[0] = attribute.substring(0,
                                       pos);
            s[1] = (pos == attribute.length()) ? "" : attribute.substring(pos + 1);
        }
        return s;
    }

    @Override
    public int priority() {
        return 10;
    }
}
