package de.pfabulist.lindwurm.eighty;

import de.pfabulist.lindwurm.eighty.path.EightyPath;
import de.pfabulist.lindwurm.eighty.watch.WatchableByteChannel;

import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.ReadOnlyFileSystemException;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
import java.time.Clock;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiFunction;

import static de.pfabulist.kleinod.nio.PathIKWID.childGetParent;
import static de.pfabulist.lindwurm.eighty.EightyUtils.throwIfPathIsNotAccessible;
import static de.pfabulist.lindwurm.eighty.path.ProviderPath.toRealPath;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;

/**
 * ** BEGIN LICENSE BLOCK *****
 * BSD License (2 clause)
 * Copyright (c) 2006 - 2015, Stephan Pfab
 * All rights reserved.
 * <p>
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * <p>
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Stephan Pfab BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * **** END LICENSE BLOCK ****
 */

@SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", "PMD.StdCyclomaticComplexity" })
public class ProviderChannel {

    @SuppressWarnings({ "PMD.NPathComplexity" })
    public static <T extends SeekableByteChannel> FileChannel newChannel( EightyPath pathArg, Set<? extends OpenOption> options, BiFunction<EightyPath, Set<OpenOption>, T> create ) throws IOException {
        final EightyFileSystem eightyFileSystem = pathArg._getFileSystem();

        EightyPath path = toRealPath( pathArg );

        Set<OpenOption> impliedOptions = new HashSet<>( options );

        if( impliedOptions.isEmpty() ) {
            impliedOptions.add( READ );
        }

        if ( impliedOptions.contains( APPEND )) {
            impliedOptions.add( WRITE );
        }

        newByteChannelParentCheck( path );

        throwIfPathIsNotAccessible( path );

        boolean creating = byteChannelCheckExistence( options, path );

        if( options.contains( APPEND ) && options.contains( READ ) ) {
            throw new IllegalArgumentException( "APPEND + READ not allowed" );
        }

        if( options.contains( APPEND ) && options.contains( TRUNCATE_EXISTING ) ) {
            throw new IllegalArgumentException( "APPEND + TRUNCATE_EXISTING not allowed" );
        }

        if( Files.isDirectory( path ) ) {
            throw new FileSystemException( path.toString() + " is a directory" );
        }

        if( impliedOptions.contains( WRITE ) ) {
            if( eightyFileSystem.isReadOnly() ) {
                throw new ReadOnlyFileSystemException();
            }

            if ( !creating && !Files.isWritable( path )) {
                throw new AccessDeniedException( "file not writable" );
            }
        }

        if ( creating ) {
            eightyFileSystem.get80().newContent( path );
        }

        T unwachedChannel = eightyFileSystem.addClosable( create.apply( path, impliedOptions ) );

        FileChannel ret = new WatchableByteChannel( unwachedChannel, () -> eightyFileSystem.signal( path, StandardWatchEventKinds.ENTRY_MODIFY ));
        if( creating ) {
            // e.g. means has a a parent
            FileTime now = FileTime.from( Clock.systemUTC().instant() );
            Files.getFileAttributeView( childGetParent( path ), BasicFileAttributeView.class ).setTimes( now, now, null );

            path._getFileSystem().signal( path, StandardWatchEventKinds.ENTRY_CREATE );
        } else if ( impliedOptions.contains( WRITE )) {
            path._getFileSystem().signal( path, StandardWatchEventKinds.ENTRY_MODIFY );
        }
        return ret;

    }

    private static boolean byteChannelCheckExistence( Set<? extends OpenOption> options, EightyPath path ) throws NoSuchFileException, FileAlreadyExistsException {
        boolean creating = false;

        if( !Files.exists( path ) ) {

            creating = true;

            if( !options.contains( WRITE ) ) {
                throw new NoSuchFileException( path.toString() );
            }

            if( !options.contains( CREATE ) && !options.contains( CREATE_NEW ) ) {
                throw new NoSuchFileException( path.toString() );
            }
        } else {

            if( options.contains( CREATE_NEW ) ) {
                throw new FileAlreadyExistsException( path.toString() );
            }
        }
        return creating;
    }

    private static void newByteChannelParentCheck( EightyPath path ) throws FileSystemException {
        @Nullable Path parent = path.getParent();

        if( parent == null ) {
            return; // todo handle
        }

        if( !Files.exists( parent ) ) {
            throw new NoSuchFileException( parent.toString() );
        }

        if( !Files.isDirectory( parent ) ) {
            throw new FileSystemException( "parent is not a directory" );
        }
    }


}
