/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.annotations.javac;

import java.io.*;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardLocation;

import net.ranides.assira.collection.maps.LazyMap;
import net.ranides.assira.io.IOStreams;
import net.ranides.assira.io.ResourceUtils;
import net.ranides.assira.text.IOStrings;
import net.ranides.assira.text.StringUtils;

final class JavaCompilationFM extends ForwardingJavaFileManager<JavaFileManager> {

    private final LazyMap<URI, JavaFileObject> files = new LazyMap<>(new HashMap<>(), key -> {
        return new CFileObject(key);
    });

    JavaCompilationFM(JavaFileManager fileManager) {
        super(fileManager);
    }

    @Override
    public boolean isSameFile(FileObject a, FileObject b) {
        // This check is less strict than what is typically done by the normal compiler file managers
        // e.g. JavacFileManager), but is actually the moral equivalent of what most of the
        // implementations do anyway. We use this check rather than just delegating to the compiler's
        // file manager because file objects for tests generally cause IllegalArgumentExceptions. 
        return a.toUri().equals(b.toUri());
    }

    @Override
    public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
        if (location.isOutputLocation()) {
            URI uri = getURI(location, packageName, relativeName);
            return files.getIfPresent(uri);
        } else {
            return super.getFileForInput(location, packageName, relativeName);
        }
    }

    @Override
    public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException {
        if (location.isOutputLocation()) {
            URI uri = getURI(location, className, kind);
            return files.getIfPresent(uri);
        } else {
            return super.getJavaFileForInput(location, className, kind);
        }
    }

    @Override
    public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException {
        URI uri = getURI(location, packageName, relativeName);
        return files.get(uri);
    }

    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        URI uri = getURI(location, className, kind);
        return files.get(uri);
    }
    
    public void exportResource(String resource) throws IOException {
        FileObject fo =  getFileForOutput(StandardLocation.CLASS_OUTPUT, "", resource, null);
        try( OutputStream os = fo.openOutputStream()) {
            IOStreams.copy(ResourceUtils.stream(resource), os);
        }
    }

    List<JavaFileObject> getOutputFiles() {
        return new ArrayList<>(files.values());
    }
    
    static String getName(Location location, String packageName, String relativeName) {
        return '/' + location.getName() + '/' + StringUtils.replace(packageName, '.', '/') + '/' + relativeName;
    }

    static String getName(Location location, String className, JavaFileObject.Kind kind) {
        return '/' + location.getName() + '/' + StringUtils.replace(className, '.', '/') + kind.extension;
    }
    
    
    private static URI getURI(Location location, String packageName, String relativeName) {
        return URI.create("mem://" + getName(location, packageName, relativeName));
    }

    private static URI getURI(Location location, String className, JavaFileObject.Kind kind) {
        return URI.create("mem://" + getName(location, className, kind.extension));
    }
    
    private static JavaFileObject.Kind deduceKind(URI uri) {
        String path = uri.getPath();
        for (JavaFileObject.Kind kind : JavaFileObject.Kind.values()) {
            if (path.endsWith(kind.extension)) {
                return kind;
            }
        }
        return JavaFileObject.Kind.OTHER;
    }

    static final class CFOString extends SimpleJavaFileObject {

        private final String source;
        private final long lastModified;

        CFOString(String fullyQualifiedName, String source) {
            super(createUri(fullyQualifiedName), JavaFileObject.Kind.SOURCE);
            this.source = source;
            this.lastModified = System.currentTimeMillis();
        }

        static URI createUri(String fullyQualifiedClassName) {
            return URI.create( StringUtils.replace(fullyQualifiedClassName, '.', '/') + JavaFileObject.Kind.SOURCE.extension);
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return source;
        }

        @Override
        public OutputStream openOutputStream() {
            throw new IllegalStateException();
        }

        @Override
        public InputStream openInputStream() {
            return new ByteArrayInputStream(source.getBytes(Charset.defaultCharset()));
        }

        @Override
        public Writer openWriter() {
            throw new IllegalStateException();
        }

        @Override
        public Reader openReader(boolean ignoreEncodingErrors) {
            return new StringReader(source);
        }

        @Override
        public long getLastModified() {
            return lastModified;
        }
    }

    static final class CFOResource extends SimpleJavaFileObject {

        private final URL url;

        CFOResource(URL resourceUrl, URI resourceUri) {
            super(resourceUri, deduceKind(resourceUri));
            this.url = resourceUrl;
        }

        CFOResource(URL resourceUrl) {
            this(resourceUrl, URI.create(resourceUrl.toString()));
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors)
                throws IOException {
            return IOStrings.read(url.openStream(), Charset.defaultCharset());
        }

        @Override
        public InputStream openInputStream() throws IOException {
            return url.openStream();
        }

        @Override
        public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
            return new InputStreamReader(openInputStream(), Charset.defaultCharset());
        }
    }

    static final class CFileObject extends SimpleJavaFileObject implements JavaFileObject {

        private long lastModified = 0L;
        private byte[] data;

        CFileObject(URI uri) {
            super(uri, deduceKind(uri));
        }
        
        @Override
        public InputStream openInputStream() throws IOException {
            checkExists();
            return new ByteArrayInputStream(data);
        }
        
        @Override
        public OutputStream openOutputStream() throws IOException {
            return new ByteArrayOutputStream() {
                @Override
                public void close() throws IOException {
                    super.close();
                    data = toByteArray();
                    lastModified = System.currentTimeMillis();
                }
            };
        }
        
        @Override
        public Reader openReader(boolean ignoreEncodingErrors) throws IOException {
            checkExists();
            return new InputStreamReader(openInputStream(), Charset.defaultCharset());
        }

        @Override
        public Writer openWriter() throws IOException {
            return new StringWriter() {
                @Override
                public void close() throws IOException {
                    super.close();
                    data = toString().getBytes(Charset.defaultCharset());
                    lastModified = System.currentTimeMillis();
                }
            };
        }
        
        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            checkExists();
            return new String(data, Charset.defaultCharset());
        }

        @Override
        public long getLastModified() {
            return lastModified;
        }

        @Override
        public boolean delete() {
            this.data = null;
            this.lastModified = 0L;
            return true;
        }
        
        private void checkExists() throws FileNotFoundException {
            if (data == null) {
                throw new FileNotFoundException();
            }
        }

    }
}
