package org.spongycastle.i18n;

import org.spongycastle.i18n.filter.Filter;
import org.spongycastle.i18n.filter.TrustedInput;
import org.spongycastle.i18n.filter.UntrustedInput;
import org.spongycastle.i18n.filter.UntrustedUrlInput;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.Format;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.TimeZone;

public class LocalizedMessage 
{

    protected final String id;
    protected final String resource;
    
    // ISO-8859-1 is the default encoding
    public static final String DEFAULT_ENCODING = "ISO-8859-1";
    protected String encoding = DEFAULT_ENCODING;
    
    protected FilteredArguments arguments;
    protected FilteredArguments extraArgs = null;
    
    protected Filter filter = null;
    
    protected ClassLoader loader = null;
    
    /**
     * Constructs a new LocalizedMessage using <code>resource</code> as the base name for the 
     * RessourceBundle and <code>id</code> as the message bundle id the resource file. 
     * @param resource base name of the resource file 
     * @param id the id of the corresponding bundle in the resource file
     * @throws NullPointerException if <code>resource</code> or <code>id</code> is <code>null</code>
     */
    public LocalizedMessage(String resource,String id) throws NullPointerException
    {
        if (resource == null || id == null)
        {
            throw new NullPointerException();
        }
        this.id = id;
        this.resource = resource;
        arguments = new FilteredArguments();
    }
    
    /**
     * Constructs a new LocalizedMessage using <code>resource</code> as the base name for the 
     * RessourceBundle and <code>id</code> as the message bundle id the resource file. 
     * @param resource base name of the resource file 
     * @param id the id of the corresponding bundle in the resource file
     * @param encoding the encoding of the resource file
     * @throws NullPointerException if <code>resource</code> or <code>id</code> is <code>null</code>
     * @throws UnsupportedEncodingException if the encoding is not supported
     */
    public LocalizedMessage(String resource,String id, String encoding) throws NullPointerException, UnsupportedEncodingException
    {
        if (resource == null || id == null)
        {
            throw new NullPointerException();
        }
        this.id = id;
        this.resource = resource;
        arguments = new FilteredArguments();
        if (!Charset.isSupported(encoding))
        {
            throw new UnsupportedEncodingException("The encoding \"" + encoding + "\" is not supported.");
        }
        this.encoding = encoding;
    }
    
    /**
     * Constructs a new LocalizedMessage using <code>resource</code> as the base name for the 
     * RessourceBundle and <code>id</code> as the message bundle id the resource file. 
     * @param resource base name of the resource file 
     * @param id the id of the corresponding bundle in the resource file
     * @param arguments an array containing the arguments for the message
     * @throws NullPointerException if <code>resource</code> or <code>id</code> is <code>null</code>
     */
    public LocalizedMessage(String resource, String id, Object[] arguments) throws NullPointerException
    {
        if (resource == null || id == null || arguments == null)
        {
            throw new NullPointerException();
        }
        this.id = id;
        this.resource = resource;
        this.arguments = new FilteredArguments(arguments);
    }
    
    /**
     * Constructs a new LocalizedMessage using <code>resource</code> as the base name for the 
     * RessourceBundle and <code>id</code> as the message bundle id the resource file. 
     * @param resource base name of the resource file 
     * @param id the id of the corresponding bundle in the resource file
     * @param encoding the encoding of the resource file
     * @param arguments an array containing the arguments for the message
     * @throws NullPointerException if <code>resource</code> or <code>id</code> is <code>null</code>
     * @throws UnsupportedEncodingException if the encoding is not supported
     */
    public LocalizedMessage(String resource, String id, String encoding, Object[] arguments) throws NullPointerException, UnsupportedEncodingException
    {
        if (resource == null || id == null || arguments == null)
        {
            throw new NullPointerException();
        }
        this.id = id;
        this.resource = resource;
        this.arguments = new FilteredArguments(arguments);
        if (!Charset.isSupported(encoding))
        {
            throw new UnsupportedEncodingException("The encoding \"" + encoding + "\" is not supported.");
        }
        this.encoding = encoding;
    }
    
    /**
     * Reads the entry <code>id + "." + key</code> from the resource file and returns a 
     * formated message for the given Locale and TimeZone.
     * @param key second part of the entry id
     * @param loc the used {@link Locale}
     * @param timezone the used {@link TimeZone}
     * @return a Strng containing the localized message
     * @throws MissingEntryException if the resource file is not available or the entry does not exist.
     */
    public String getEntry(String key,Locale loc, TimeZone timezone) throws MissingEntryException
    {
        String entry = id;
        if (key != null)
        {
            entry += "." + key;
        }
        
        try
        {
            ResourceBundle bundle;
            if (loader == null)
            {
                bundle = ResourceBundle.getBundle(resource,loc);
            }
            else
            {
                bundle = ResourceBundle.getBundle(resource, loc, loader);
            }
            String result = bundle.getString(entry);
            if (!encoding.equals(DEFAULT_ENCODING))
            {
                result = new String(result.getBytes(DEFAULT_ENCODING), encoding);
            }
            if (!arguments.isEmpty())
            {
                result = formatWithTimeZone(result,arguments.getFilteredArgs(loc),loc,timezone);
            }
            result = addExtraArgs(result, loc);
            return result;
        }
        catch (MissingResourceException mre)
        {
            throw new MissingEntryException("Can't find entry " + entry + " in resource file " + resource + ".",
                    resource,
                    entry,
                    loc,
                    loader != null ? loader : this.getClassLoader()); 
        }
        catch (UnsupportedEncodingException use)
        {
            // should never occur - cause we already test this in the constructor
            throw new RuntimeException(use);
        }
    }
    
    protected String formatWithTimeZone(
            String template,
            Object[] arguments, 
            Locale locale,
            TimeZone timezone) 
    {
        MessageFormat mf = new MessageFormat(" ");
        mf.setLocale(locale);
        mf.applyPattern(template);
        if (!timezone.equals(TimeZone.getDefault())) 
        {
            Format[] formats = mf.getFormats();
            for (int i = 0; i < formats.length; i++) 
            {
                if (formats[i] instanceof DateFormat) 
                {
                    DateFormat temp = (DateFormat) formats[i];
                    temp.setTimeZone(timezone);
                    mf.setFormat(i,temp);
                }
            }
        }
        return mf.format(arguments);
    }
    
    protected String addExtraArgs(String msg, Locale locale)
    {
        if (extraArgs != null)
        {
            StringBuffer sb = new StringBuffer(msg);
            Object[] filteredArgs = extraArgs.getFilteredArgs(locale);
            for (int i = 0; i < filteredArgs.length; i++)
            {
                sb.append(filteredArgs[i]);
            }
            msg = sb.toString();
        }
        return msg;
    }
    
    /**
     * Sets the {@link Filter} that is used to filter the arguments of this message
     * @param filter the {@link Filter} to use. <code>null</code> to disable filtering.
     */
    public void setFilter(Filter filter)
    {
        arguments.setFilter(filter);
        if (extraArgs != null)
        {
            extraArgs.setFilter(filter);
        }
        this.filter = filter;
    }
    
    /**
     * Returns the current filter.
     * @return the current filter
     */
    public Filter getFilter()
    {
        return filter;
    }
    
    /**
     * Set the {@link ClassLoader} which loads the resource files. If it is set to <code>null</code>
     * then the default {@link ClassLoader} is used. 
     * @param loader the {@link ClassLoader} which loads the resource files
     */
    public void setClassLoader(ClassLoader loader)
    {
        this.loader = loader;
    }
    
    /**
     * Returns the {@link ClassLoader} which loads the resource files or <code>null</code>
     * if the default ClassLoader is used.
     * @return the {@link ClassLoader} which loads the resource files
     */
    public ClassLoader getClassLoader()
    {
        return loader;
    }
    
    /**
     * Returns the id of the message in the resource bundle.
     * @return the id of the message
     */
    public String getId()
    {
        return id;
    }
    
    /**
     * Returns the name of the resource bundle for this message
     * @return name of the resource file
     */
    public String getResource()
    {
        return resource;
    }
    
    /**
     * Returns an <code>Object[]</code> containing the message arguments.
     * @return the message arguments
     */
    public Object[] getArguments()
    {
        return arguments.getArguments();
    }
    
    /**
     * 
     * @param extraArg
     */
    public void setExtraArgument(Object extraArg)
    {
        setExtraArguments(new Object[] {extraArg});
    }
    
    /**
     * 
     * @param extraArgs
     */
    public void setExtraArguments(Object[] extraArgs)
    {
        if (extraArgs != null)
        {
            this.extraArgs = new FilteredArguments(extraArgs);
            this.extraArgs.setFilter(filter);
        }
        else
        {
            this.extraArgs = null;
        }
    }
    
    /**
     * 
     * @return
     */
    public Object[] getExtraArgs()
    {
        return (extraArgs == null) ? null : extraArgs.getArguments();
    }
    
    protected class FilteredArguments
    {
        protected static final int NO_FILTER = 0;
        protected static final int FILTER = 1;
        protected static final int FILTER_URL = 2;
        
        protected Filter filter = null;
        
        protected boolean[] isLocaleSpecific;
        protected int[] argFilterType;
        protected Object[] arguments;
        protected Object[] unpackedArgs;
        protected Object[] filteredArgs;
        
        FilteredArguments()
        {
            this(new Object[0]);
        }
        
        FilteredArguments(Object[] args)
        {
            this.arguments = args;
            this.unpackedArgs = new Object[args.length];
            this.filteredArgs = new Object[args.length];
            this.isLocaleSpecific = new boolean[args.length];
            this.argFilterType = new int[args.length];
            for (int i = 0; i < args.length; i++)
            {
                if (args[i] instanceof TrustedInput)
                {
                    this.unpackedArgs[i] = ((TrustedInput) args[i]).getInput();
                    argFilterType[i] = NO_FILTER;
                }
                else if (args[i] instanceof UntrustedInput)
                {
                    this.unpackedArgs[i] = ((UntrustedInput) args[i]).getInput();
                    if (args[i] instanceof UntrustedUrlInput)
                    {
                        argFilterType[i] = FILTER_URL;
                    }
                    else
                    {
                        argFilterType[i] = FILTER;
                    }
                }
                else
                {
                    this.unpackedArgs[i] = args[i];
                    argFilterType[i] = FILTER;
                }
                
                // locale specific
                this.isLocaleSpecific[i] = (this.unpackedArgs[i] instanceof LocaleString);
            }
        }
        
        public boolean isEmpty()
        {
            return unpackedArgs.length == 0;
        }
        
        public Object[] getArguments()
        {
            return arguments;
        }
        
        public Object[] getFilteredArgs(Locale locale)
        {
            Object[] result = new Object[unpackedArgs.length];
            for (int i = 0; i < unpackedArgs.length; i++)
            {
                Object arg;
                if (filteredArgs[i] != null)
                {
                    arg = filteredArgs[i];
                }
                else
                {
                    arg = unpackedArgs[i];
                    if (isLocaleSpecific[i])
                    {
                        // get locale
                        arg = ((LocaleString) arg).getLocaleString(locale);
                        arg = filter(argFilterType[i], arg);
                    }
                    else
                    {
                        arg = filter(argFilterType[i], arg);
                        filteredArgs[i] = arg;
                    }
                }
                result[i] = arg;
            }
            return result;
        }
        
        private Object filter(int type, Object obj)
        {
            if (filter != null)
            {
                Object o = (null == obj) ? "null" : obj;
                switch (type)
                {
                case NO_FILTER:
                    return o;
                case FILTER:
                    return filter.doFilter(o.toString());
                case FILTER_URL:
                    return filter.doFilterUrl(o.toString());
                default:
                    return null;
                }
            }
            else
            {
                return obj;
            }
        }

        public Filter getFilter()
        {
            return filter;
        }

        public void setFilter(Filter filter)
        {
            if (filter != this.filter)
            {
                for (int i = 0; i < unpackedArgs.length; i++)
                {
                    filteredArgs[i] = null;
                }
            }
            this.filter = filter;
        }
        
    }
    
    public String toString()
    {
        StringBuffer sb = new StringBuffer();
        sb.append("Resource: \"").append(resource);
        sb.append("\" Id: \"").append(id).append("\"");
        sb.append(" Arguments: ").append(arguments.getArguments().length).append(" normal");
        if (extraArgs != null && extraArgs.getArguments().length > 0)
        {
            sb.append(", ").append(extraArgs.getArguments().length).append(" extra");
        }
        sb.append(" Encoding: ").append(encoding);
        sb.append(" ClassLoader: ").append(loader);
        return sb.toString();
    }

}
