/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.jena.tdb.base.file;

import java.io.ByteArrayOutputStream ;
import java.io.File ;
import java.io.FileNotFoundException ;
import java.io.IOException ;
import java.io.InputStream ;
import java.io.PrintStream ;
import java.util.Comparator ;
import java.util.Objects;
import java.util.Properties ;
import java.util.SortedSet ;
import java.util.TreeSet ;

import org.apache.jena.atlas.lib.* ;
import org.apache.jena.tdb.TDBException ;
import org.apache.jena.tdb.sys.Names ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/** Abstraction and many convenience operations on metadata. 
 * Metadata is recorded in Java properties style - not RDF - because it's relative to the file or context used.
 * Keys and values are always strings.
 */
public class MetaFile implements Sync, Closeable
{
    private static Comparator<String> comparator = new ComparatorKeys() ;
    private static Logger log = LoggerFactory.getLogger(MetaFile.class) ;
    private String metaFilename = null ;
    private Properties properties = null ;
    private String label = null ;
    private boolean changed = false ;
    private boolean closed = false ;
    
    /** Create a MetaFile
     *  
     * @param label     Convenience label.
     * @param fn        On disk filename @link{Names.mem} for in-memory
     */
    public MetaFile(String label, String fn)
    {
        this.label = label ;
        this.metaFilename = fn ;

        if ( fn == null || Names.isMem(fn) )
            // In-memory.
            return ;
        
        // Make absolute (current directory may change later)
        if ( ! fn.endsWith(Names.extMeta) )
            fn = fn+"."+Names.extMeta ;
        File f = new File(fn) ;
        this.metaFilename = f.getAbsolutePath() ;
        // Does not load the details yet.
        // JDI
        ensureInit() ; 
    }
    
    private void ensureInit()
    { 
        if ( properties == null )
        {
            properties = new PropertiesSorted(comparator) ;
            if ( metaFilename != null )
                loadProperties() ;
        }
    }
    
    /** Does this metafile exist on disk? (In-memory MetaFiles always exist) */
    public boolean existsMetaData()
    {
        if ( isMem() )
            return true ;
        File f = new File(metaFilename) ;
        if ( f.isDirectory() )
            log.warn("Metadata file clashes with a directory") ;
        return f.exists() && f.isFile() ;
    }
    
    public String getFilename()         { return metaFilename ; } 

    /** Test for the presence of a property */
    public boolean hasProperty(String key)
    {
        return _getProperty(key, null) != null ;
    }

    /** Get the property value or null. */
    public String getProperty(String key)
    {
        return _getProperty(key, null) ;
    }
    
    /** Get the property value or return supplied default. */
    public String getProperty(String key, String defaultString)
    {
        return _getProperty(key, defaultString) ;
    }

    /** Get the property value and parse as an integer */
    public int getPropertyAsInteger(String key)
    {
        return Integer.parseInt(_getProperty(key, null)) ;
    }
    
    /** Get the property value and parse as an integer or return default value. */
    public int getPropertyAsInteger(String key, int defaultValue)
    {
        String x = getProperty(key) ;
        if ( x == null )
            return defaultValue ;
        return Integer.parseInt(x) ;
    }

    /** Get property as a string and split on ",". */
    public String[] getPropertySplit(String key)
    {
        String str = getProperty(key) ;
        if ( str == null )
            return null ;
        return str.split(",") ;
    }
    
    /** Get property as a string and split on ",", using the default string if not present in the MetaFile. */
    public String[] getPropertySplit(String key, String defaultString)
    {
        String str = getProperty(key, defaultString) ;
        return str.split(",") ;
    }
    
    /** Set property */
    public void setProperty(String key, String value)
    {
        _setProperty(key, value) ;
    }
    
    /** Set property, turning integer into a string. */
    public void setProperty(String key, int value)
    {
        _setProperty(key, Integer.toString(value)) ;
    }
    
    /** Test whether a property has a value.  Null tests equal to not present. */
    public boolean propertyEquals(String key, String value)
    {
        return Objects.equals(getProperty(key), value) ;
    }

    /** Set property if not already set. */
    public void ensurePropertySet(String key, String expected)
    {
        getOrSetDefault(key, expected) ;
    }

    /** Get property or the default value - also set the default value if not present */
    public String getOrSetDefault(String key, String expected)
    {
        String x = getProperty(key) ;
        if ( x == null )
        {
            setProperty(key, expected) ;
            x = expected ;
        }
        return x ;
    }
    
    /** Check property is an expected value or set if missing */
    public void checkOrSetMetadata(String key, String expected)
    {
        String x = getProperty(key) ;
        if ( x == null )
        {
            setProperty(key, expected) ;
            return ; 
        }
        if ( x.equals(expected) )
            return ;
        
        inconsistent(key, x, expected) ; 
    }

    /** Check property has the value given - throw exception if not. */
    public void checkMetadata(String key, String expected)
    {
        String value = getProperty(key) ;
        
        if ( ! Objects.equals(value, value) )
            inconsistent(key, value, expected) ;
    }

    private static void inconsistent(String key, String actual, String expected) 
    {
        String msg = String.format("Inconsistent: key=%s value=%s expected=%s", 
                                   key, 
                                   (actual==null?"<null>":actual),
                                   (expected==null?"<null>":expected) ) ;
        throw new MetaFileException(msg) ; 
    }
    
    /** Clear all properties. */
    public void clear()
    {
        _clear() ;
    }

    // ---- All get/set access through these  operations
    private String _getProperty(String key, String dft)
    {
        ensureInit() ;
        return properties.getProperty(key, dft) ;
    }
    
    private void _setProperty(String key, String value)
    {
        ensureInit() ;
        properties.setProperty(key, value) ;
        changedEvent() ;
    }
    
    /** Clear all properties. */
    private void _clear()
    {
        ensureInit() ;
        properties.clear() ;
        changedEvent() ;
    }

    private void changedEvent() { changed = true ; }
    // ----
    
    private boolean isMem() { return Names.isMem(metaFilename) ; }
    
    /** Write to backing file if changed */
    public void flush()
    {
        if ( log.isDebugEnabled() )
            log.debug("Flush metadata ("+changed+"): "+this.label) ;
        if ( ! changed )
            return ;
        
        
        if ( log.isDebugEnabled() )
        {
            ByteArrayOutputStream out = new ByteArrayOutputStream() ;
            PrintStream ps = new PrintStream(out) ;
            properties.list(ps) ;
            ps.flush() ;
            log.debug("\n"+out.toString()) ;
        }
        
        //properties.list(System.out) ;
        
        saveProperties() ;
        changed = false ;
    }

    private void saveProperties()
    {
        if ( isMem() )
            return ;
        String str = label ;
        if ( str == null )
            str = metaFilename ;
        str = "Metadata: "+str ;

        try {
            PropertyUtils.storeToFile(properties, str, metaFilename) ;
        } 
        catch (IOException ex)
        {
            log.error("Failed to store properties: "+metaFilename, ex) ;
        }
    }

    
    private void loadProperties()
    {
        if ( isMem() )
        {
            properties = new Properties() ;
            return ;
        }
        
        if ( properties == null )
            properties = new Properties() ;
        
        // if ( metaFilename == null )
        InputStream in = null ;
        try { 
            //  Copes with UTF-8 for Java5. 
            PropertyUtils.loadFromFile(properties, metaFilename) ;
        }
        catch (FileNotFoundException ex) {} 
        catch (IOException ex)
        {
            log.error("Failed to load properties: "+metaFilename, ex) ;
        }
    }
    
    /** Debugging */
    public void dump(PrintStream output)
    {
        output.println("Metafile: "+metaFilename) ;
        output.println("Label: "+label) ;
        output.println("Status: "+(changed?"changed":"unchanged")) ;
        
        if ( properties == null )
        {
            output.println("#<null>") ;
            return ;
        }
        // properties.list() ;
        SortedSet<Object> x = new TreeSet<>() ;
        x.addAll(properties.keySet()) ;
        
        for ( Object k : x )
        {
            String key = (String)k ;
            String value = properties.getProperty(key) ;
            output.print(key) ;
            output.print("=") ;
            output.print(value) ;
            output.println() ;
        }
    }

    @Override
    public void sync()                  { flush() ; }
    
    @Override
    public void close()
    {
        flush() ;
        closed = true ;
        metaFilename = null ;
        properties = null ;

    }

    private static class ComparatorKeys implements Comparator<String>
    {
        @Override
        public int compare(String o1, String o2)
        {
            return - o1.compareTo(o2) ;
        }
        
    }
    
    private static class MetaFileException extends TDBException
    {
        MetaFileException(String msg) { super(msg) ; }
    }

}
