// Copyright 2008-2012 severally by the contributors
//
// Licensed 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 net.sf.practicalxml.converter.internal;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;


import net.sf.practicalxml.XmlUtil;
import net.sf.practicalxml.converter.ConversionException;


/**
 *  Handles conversion of objects that have a simple string representation.
 *  This includes primitives, and primitive wrappers, but also some complex
 *  types such as <code>java.lang.Class</code> and <code>java.io.File</code>.
 *  <p>
 *  A single instance of this object will be associated with each converter.
 */
public class JavaStringConversions
{
    private boolean _useXsdFormat;


    /**
     *  @param useXsdFormat If <code>true</code>, conversions will use the
     *                      format defined in the XML Schema specification
     *                      (if any). If <code>false</code>, or there is no
     *                      specification, will use <code>toString()</code>
     *                      for stringification, and parse appropriately.
     */
    public JavaStringConversions(boolean useXsdFormat)
    {
        _useXsdFormat = useXsdFormat;
    }


//----------------------------------------------------------------------------
//  Lookup table for common conversions
//----------------------------------------------------------------------------

    private static Map<Class<?>,ConversionHandler<?>> _handlers
            = new HashMap<Class<?>,ConversionHandler<?>>();
    static
    {
        _handlers.put(String.class,      new StringConversionHandler());
        _handlers.put(Character.class,   new CharacterConversionHandler());
        _handlers.put(Boolean.class,     new BooleanConversionHandler());
        _handlers.put(Byte.class,        new ByteConversionHandler());
        _handlers.put(Short.class,       new ShortConversionHandler());
        _handlers.put(Integer.class,     new IntegerConversionHandler());
        _handlers.put(Long.class,        new LongConversionHandler());
        _handlers.put(Float.class,       new FloatConversionHandler());
        _handlers.put(Double.class,      new DoubleConversionHandler());
        _handlers.put(BigInteger.class,  new BigIntegerConversionHandler());
        _handlers.put(BigDecimal.class,  new BigDecimalConversionHandler());

        _handlers.put(Boolean.TYPE,      new BooleanConversionHandler());
        _handlers.put(Byte.TYPE,         new ByteConversionHandler());
        _handlers.put(Short.TYPE,        new ShortConversionHandler());
        _handlers.put(Integer.TYPE,      new IntegerConversionHandler());
        _handlers.put(Long.TYPE,         new LongConversionHandler());
        _handlers.put(Float.TYPE,        new FloatConversionHandler());
        _handlers.put(Double.TYPE,       new DoubleConversionHandler());

        _handlers.put(Class.class,       new ClassConversionHandler());
        _handlers.put(File.class,        new FileConversionHandler());
        _handlers.put(Locale.class,      new LocaleConversionHandler());
        _handlers.put(TimeZone.class,    new TimeZoneConversionHandler());
    }


//----------------------------------------------------------------------------
//  Public Methods
//----------------------------------------------------------------------------

    /**
     *  Determines whether the passed object has a simple string representation.
     */
    public boolean isConvertableToString(Object obj)
    {
        if (obj == null)
            return false;

        return isConvertableToString(obj.getClass());
    }


    /**
     *  Determines whether the objects of the specified class have a simple string
     *  representation.
     */
    public boolean isConvertableToString(Class<?> klass)
    {
        return getHandler(klass) != null;
    }


    /**
     *  Converts a Java primitive object to a string representation. Returns
     *  <code>null</code> if passed <code>null</code>.
     *
     *  @param  value           The object to convert.
     *
     *  @throws ConversionException if the passed object does not have a string
     *          representation (ie, is not a primitive value).
     */
    public String stringify(Object value)
    {
        if (value == null)
            return null;

        try
        {
            return getHandler(value.getClass()).stringify(value, _useXsdFormat);
        }
        catch (Exception ee)
        {
            if (ee instanceof ConversionException)
                throw (ConversionException)ee;
            throw new ConversionException("unable to convert: " + value, ee);
        }
    }


    /**
     *  Parses the passed string as a Java primitive object of the specified
     *  type. Will attempt to use the built-in parsing functions for the type,
     *  or a format defined by XML Schema for the type that would be returned
     *  by {@link TypeUtils#class2type} for the passed class. Returns <code>null</code>
     *  if passed <code>null</code>.
     *
     *  @param  value           String representation.
     *  @param  klass           Desired class for resulting object.
     *
     *  @throws ConversionException if unable to parse.
     */
    public Object parse(String value, Class<?> klass)
    {
        if (value == null)
            return null;

        try
        {
            return getHandler(klass).parse(value, _useXsdFormat);
        }
        catch (Exception ee)
        {
            if (ee instanceof ConversionException)
                throw (ConversionException)ee;
            throw new ConversionException("unable to parse: " + value, ee);
        }
    }


//----------------------------------------------------------------------------
//  Internals -- conversion handlers
//----------------------------------------------------------------------------

    /**
     *  Finds the helper for a given class, returning <code>null</code> if there
     *  is no helper. This will be a direct lookup on the table, with a fallback
     *  for non-final classes.
     */
    private static ConversionHandler<Object> getHandler(Class<?> klass)
    {
       ConversionHandler<Object> handler = (ConversionHandler<Object>)_handlers.get(klass);
       if (handler != null)
            return handler;

       // the following typically have implementation classes, so we won't find
       // them with the direct lookup
       if (TimeZone.class.isAssignableFrom(klass))
           return (ConversionHandler<Object>)_handlers.get(TimeZone.class);

        return null;
    }


    /**
     *  Each primitive class has its own conversion handler that is responsible
     *  for converting to/from a string representation. Handlers are guaranteed
     *  to receive non-null objects/strings.
     *  <p>
     *  This interface is parameterized so that the compiler will generate
     *  bridge methods for implementation classes. Elsewhere, we don't care
     *  about parameterization, so wildcard or drop it (see {@link #getHelper}).
     *  <p>
     *  Implementation classes are permitted to throw any exception; caller is
     *  expected to catch them and translate to a {@link ConversionException}.
     */
    private static interface ConversionHandler<T>
    {
        public String stringify(T obj, boolean useXsdFormat);
        public T parse(String str, boolean useXsdFormat);
    }


    private static class StringConversionHandler
    implements ConversionHandler<String>
    {
        public String stringify(String obj, boolean useXsdFormat)
        {
            return String.valueOf(obj);
        }

        public String parse(String str, boolean useXsdFormat)
        {
            return str;
        }
    }


    private static class CharacterConversionHandler
    implements ConversionHandler<Character>
    {
        private final Character NUL = Character.valueOf('\0');

        public String stringify(Character obj, boolean useXsdFormat)
        {
            if (obj.equals(NUL))
                return "";
            return obj.toString();
        }

        public Character parse(String str, boolean useXsdFormat)
        {
            if (str.length() == 0)
                return NUL;
            if (str.length() > 1)
                throw new ConversionException(
                        "attempted to convert multi-character string: \"" + str + "\"");
            return Character.valueOf(str.charAt(0));
        }
    }


    private static class BooleanConversionHandler
    implements ConversionHandler<Boolean>
    {
        public String stringify(Boolean obj, boolean useXsdFormat)
        {
            return useXsdFormat
                 ? XmlUtil.formatXsdBoolean(obj.booleanValue())
                 : obj.toString();
        }

        public Boolean parse(String str, boolean useXsdFormat)
        {
            return useXsdFormat
                 ? XmlUtil.parseXsdBoolean(str)
                 : Boolean.parseBoolean(str);
        }
    }


    private static class ByteConversionHandler
    implements ConversionHandler<Byte>
    {
        public String stringify(Byte obj, boolean useXsdFormat)
        {
            return obj.toString();
        }

        public Byte parse(String str, boolean useXsdFormat)
        {
            return Byte.valueOf(str.trim());
        }
    }


    private static class ShortConversionHandler
    implements ConversionHandler<Short>
    {
        public String stringify(Short obj, boolean useXsdFormat)
        {
            return obj.toString();
        }

        public Short parse(String str, boolean useXsdFormat)
        {
            return Short.valueOf(str.trim());
        }
    }


    private static class IntegerConversionHandler
    implements ConversionHandler<Integer>
    {
        public String stringify(Integer obj, boolean useXsdFormat)
        {
            return obj.toString();
        }

        public Integer parse(String str, boolean useXsdFormat)
        {
            return Integer.valueOf(str.trim());
        }
    }


    private static class LongConversionHandler
    implements ConversionHandler<Long>
    {
        public String stringify(Long obj, boolean useXsdFormat)
        {
            return obj.toString();
        }

        public Long parse(String str, boolean useXsdFormat)
        {
            return Long.valueOf(str.trim());
        }
    }


    private static class FloatConversionHandler
    implements ConversionHandler<Float>
    {
        public String stringify(Float obj, boolean useXsdFormat)
        {
            return useXsdFormat
                 ? XmlUtil.formatXsdDecimal(obj)
                 : obj.toString();
        }

        public Float parse(String str, boolean useXsdFormat)
        {
            return Float.valueOf(str.trim());
        }
    }


    private static class DoubleConversionHandler
    implements ConversionHandler<Double>
    {
        public String stringify(Double obj, boolean useXsdFormat)
        {
            return useXsdFormat
                 ? XmlUtil.formatXsdDecimal(obj)
                 : obj.toString();
        }

        public Double parse(String str, boolean useXsdFormat)
        {
            return Double.valueOf(str.trim());
        }
    }


    private static class BigIntegerConversionHandler
    implements ConversionHandler<BigInteger>
    {
        public String stringify(BigInteger obj, boolean useXsdFormat)
        {
            return obj.toString();
        }

        public BigInteger parse(String str, boolean useXsdFormat)
        {
            return new BigInteger(str.trim());
        }
    }


    private static class BigDecimalConversionHandler
    implements ConversionHandler<BigDecimal>
    {
        public String stringify(BigDecimal obj, boolean useXsdFormat)
        {
            return obj.toString();
        }

        public BigDecimal parse(String str, boolean useXsdFormat)
        {
            return new BigDecimal(str.trim());
        }
    }


    @SuppressWarnings("rawtypes")
    private static class ClassConversionHandler
    implements ConversionHandler<Class>
    {
        public String stringify(Class obj, boolean useXsdFormat)
        {
            return obj.getName();
        }

        public Class parse(String str, boolean useXsdFormat)
        {
            try
            {
                return Class.forName(str);
            }
            catch (Exception ex)
            {
                throw new ConversionException("invalid classname: " + str, ex);
            }
        }
    }


    private static class FileConversionHandler
    implements ConversionHandler<File>
    {
        public String stringify(File obj, boolean useXsdFormat)
        {
            try
            {
                return obj.getCanonicalPath();
            }
            catch (IOException ex)
            {
                throw new RuntimeException("unable to convert file to string", ex);
            }
        }

        public File parse(String str, boolean useXsdFormat)
        {
            try
            {
                return new File(str);
            }
            catch (Exception ex)
            {
                throw new ConversionException("invalid filename: " + str, ex);
            }
        }
    }


    private static class TimeZoneConversionHandler
    implements ConversionHandler<TimeZone>
    {
        public String stringify(TimeZone obj, boolean useXsdFormat)
        {
            return obj.getID();
        }

        public TimeZone parse(String str, boolean useXsdFormat)
        {
            try
            {
                return TimeZone.getTimeZone(str);
            }
            catch (Exception ex)
            {
                throw new ConversionException("invalid timezone ID: " + str, ex);
            }
        }
    }


    private static class LocaleConversionHandler
    implements ConversionHandler<Locale>
    {
        public String stringify(Locale obj, boolean useXsdFormat)
        {
            // note: format is defined by API docs
            return obj.toString();
        }

        public Locale parse(String str, boolean useXsdFormat)
        {
            try
            {
                String[] cmp = str.split("_");
                switch (cmp.length)
                {
                    case 1 :
                        return new Locale(cmp[0]);
                    case 2 :
                        return new Locale(cmp[0], cmp[1]);
                    case 3 :
                        return new Locale(cmp[0], cmp[1], cmp[2]);
                    default :
                        throw new IllegalArgumentException();
                }
            }
            catch (Exception ex)
            {
                throw new ConversionException("invalid locale string: " + str, ex);
            }
        }
    }
}
