/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.io.uri.impl;

import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.io.IOHandle;
import net.ranides.assira.io.IOStreams;
import net.ranides.assira.io.uri.URIBuilder;
import net.ranides.assira.io.uri.URIFlags;
import net.ranides.assira.io.uri.URIHandle;
import net.ranides.assira.io.uri.URIHandler;
import net.ranides.assira.io.uri.URIResolver;
import net.ranides.assira.io.uri.URITime;
import net.ranides.assira.io.uri.URIUtils;
import net.ranides.assira.text.StringTraits;
import net.ranides.assira.text.StringUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.NotDirectoryException;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Supported schemes: jar
 *
 * Supported format: jar://file://file.jar!/resource/path
 *
 * Please note:
 * As every URIHandler, it should not be constructed directly, but connected with some instance of URIResolver.
 */
public class CJarHandler implements URIHandler {
    
    private final JARManager man = new JARManager();
    
    private final URIResolver resolver;

    /**
     * Creates default handler. It is used by default URIResolver
     */
    public CJarHandler() {
        this.resolver = URIResolver.DEFAULT;
    }

    /**
     * Creates handler connected with specified resolver.
     *
     * @param resolver resolver
     */
    public CJarHandler(URIResolver resolver) {
        this.resolver = resolver;
    }

    @Override
    public URIHandle resolve(URI uri) {
        return new CJarHandle(uri);
    }

    @Override
    public Collection<String> schemes() {
        return Arrays.asList("jar");
    }

    private static String[] split(URI uri) {
        String file = URIUtils.getFile(uri);
        return StringUtils.removePrefix(file,"file:").split("!/", 2);
    }

    private static URI concat(File jar, String file) {
        return new URIBuilder().scheme("jar").path(jar + "!/" + file).build();
    }
    
    private class CJarHandle extends CHandle {
        
        public final URI uri;
        public final File jar;
        public final String file;
        public final Charset charset;
        public final boolean text;
        
        public final IOHandle<ZipEntry> entry;

        public CJarHandle(URI uri) {
            this(uri, URIUtils.getParam(uri, "charset"), split(uri));
        }

        private CJarHandle(URI uri, Ref<String> charset, String... path) {
            this.uri = uri;
            this.jar = new File(path[0]);
            this.file = path[1];
            this.charset = Charset.forName(charset.orElse("UTF-8"));
            this.text = charset.isPresent();
            this.entry = IOHandle.shared(() -> {
                ZipEntry edir = man.shared(jar).getEntry(file+"/");
                if(edir != null) {
                    return edir;
                }
                return man.shared(jar).getEntry(file);
            });
            man.open(jar);

        }
        
        public CJarHandle(File jar, String file) {
            this(concat(jar, file), Ref.empty(), jar.toString(), file);
        }
        
        @Override
        public void close() throws IOException {
            man.close(jar);
        }
        
        @Override
        public URIResolver resolver() {
            return CJarHandler.this.resolver;
        }
        
        @Override
        public String scheme() {
            return "jar";
        }

        @Override
        public URI uri() {
            return uri;
        }

        @Override
        public Charset charset() {
            return charset;
        }
        
        @Override
        public boolean exists() throws IOException {
            return null != entry.get();
        }
        
        @Override
        public Set<URIFlags> flags() throws IOException {
            ZipEntry attr = entry.get();
            Set<URIFlags> flags = URIFlags.collect();
            
            if(null == attr) {
                return flags;
            }
            if(attr.isDirectory()) {
                flags.add(URIFlags.DIR);
            } else {
                flags.add(URIFlags.FILE);
                flags.add(URIFlags.READABLE);
                if(-1 != attr.getSize()) {
                    flags.add(URIFlags.SIZE);
                }
                if(text) {
                    flags.add(URIFlags.TEXT);
                } else {
                    flags.add(URIFlags.BINARY);
                }
            }
            return flags;
        }
        
        @Override
        public Instant time(URITime ut) throws IOException {
            ZipEntry attr = info();
            FileTime ft;
            switch(ut) {
                case ACCESSED:
                    ft = attr.getLastAccessTime();
                    break;
                case CREATED:
                    ft = attr.getCreationTime();
                    break;
                case MODIFIED:
                    ft = attr.getLastModifiedTime();
                    break;
                default:
                    throw new UnsupportedOperationException("Not supported attribute: " + ut);
            }
            if(null == ft) {
                throw new UnsupportedOperationException("Unknown value of attribute: " + ut);
            }
            return ft.toInstant();
        }

        @Override
        public long size() throws IOException {
            return info().isDirectory() ? scan().size() : info().getSize();
        }
        
        @Override
        public URIHandle parent() {
            return new CJarHandle(jar, ValueUtils.or(new File(file).getParent(),""));
        }
        
        @Override
        public CQuery<URIHandle> scan() throws IOException {
            if(!file.isEmpty() && !info().isDirectory() ) {
                throw new NotDirectoryException(file);
            }
            return CQuery.from()
                .stream(() -> man.shared(jar).stream().filter(e -> isChildren(StringUtils.trim(file,'/'), e)))
                .map(e -> new CJarHandle(jar, e.getName()));
        }

        private boolean isChildren(String parent, ZipEntry e) {
            String name = StringUtils.trim(e.getName(),'/');
            if( parent.equals(name)) {
                return false;
            }
            if( !name.startsWith(parent)) {
                return false;
            }
            String subpath = StringUtils.trim(name.substring(file.length()),'/');
            return !StringTraits.contains(subpath,'/');
        }

        @Override
        public InputStream istream() throws IOException {
            man.open(jar);
            InputStream is = man.shared(jar).getInputStream(entry.get());
            return IOStreams.onClose(is, () -> {
                man.close(jar);
            });
        }
        
        private ZipEntry info() throws IOException {
            ZipEntry attr = entry.get();
            if(null == attr) {
                throw new FileNotFoundException("File not found: " + file);
            }
            return attr;
        }
        
    }

    private static final class JARManager extends CHandleManager<File, ZipFile> {
        
        @Override
        protected void disconnect(ZipFile zip) throws IOException {
            zip.close();
        }

        @Override
        protected ZipFile reset(ZipFile zip) {
            return zip;
        }

        @Override
        protected ZipFile connect(File path) throws IOException {
            return new ZipFile(path);
        }
        
    }
}
