////////////////////////////////////////////////////////////////////////////////
//
//  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 mx.formatters
{

import mx.core.mx_internal;
/* import mx.managers.ISystemManager;
import mx.managers.SystemManager; */

use namespace mx_internal;

//[ResourceBundle("SharedResources")]

[Alternative(replacement="spark.formatters.DateTimeFormatter", since="4.5")]
/**
 *  The DateFormatter class uses a format String to return a formatted date and time String
 *  from an input String or a Date object.
 *  You can create many variations easily, including international formats.
 *
 *  <p>If an error occurs, an empty String is returned and a String describing 
 *  the error is saved to the <code>error</code> property. The <code>error</code> 
 *  property can have one of the following values:</p>
 *
 *  <ul>
 *    <li><code>"Invalid value"</code> means a value that is not a Date object or a 
 *    is not a recognized String representation of a date is
 *    passed to the <code>format()</code> method. (An empty argument is allowed.)</li>
 *    <li> <code>"Invalid format"</code> means either the <code>formatString</code> 
 *    property is set to empty (""), or there is less than one pattern letter 
 *    in the <code>formatString</code> property.</li>
 *  </ul>
 *
 *  <p>The <code>parseDateString()</code> method uses the mx.formatters.DateBase class
 *  to define the localized string information required to convert 
 *  a date that is formatted as a String into a Date object.</p>
 *  
 *  @mxml
 *  
 *  <p>You use the <code>&lt;mx:DateFormatter&gt;</code> tag
 *  to render date and time Strings from a Date object.</p>
 *
 *  <p>The <code>&lt;mx:DateFormatter&gt;</code> tag
 *  inherits all of the tag attributes  of its superclass,
 *  and adds the following tag attributes:</p>
 *  
 *  <pre>
 *  &lt;mx:DateFormatter
 *    formatString="Y|M|D|A|E|H|J|K|L|N|S|Q|O|Z"
 *   /> 
 *  </pre>
 *  
 *  @includeExample examples/DateFormatterExample.mxml
 *  
 *  @see mx.formatters.DateBase
 *  
 *  @langversion 3.0
 *  @playerversion Flash 9
 *  @playerversion AIR 1.1
 *  @productversion Royale 0.9.3
 */
public class DateFormatter extends Formatter
{
    //include "../core/Version.as";

    //--------------------------------------------------------------------------
    //
    //  Class constants
    //
    //--------------------------------------------------------------------------

    /**
     *  @private    
     */
    private static const VALID_PATTERN_CHARS:String = "Y,M,D,A,E,H,J,K,L,N,S,Q,O,Z";
    
    //--------------------------------------------------------------------------
    //
    //  Class methods
    //
    //--------------------------------------------------------------------------

    /**
     *  Converts a date that is formatted as a String into a Date object.
     *  Month and day names must match the names in mx.formatters.DateBase.
     *
     *  The hour value in the String must be between 0 and 23, inclusive. 
     *  The minutes and seconds value must be between 0 and 59, inclusive.
     *  The following example uses this method to create a Date object:
     *
     *  <pre>
     *  var myDate:Date = DateFormatter.parseDateString("2009-12-02 23:45:30"); </pre>
	 * 
	 *  The optional format property is use to work out which is likly to be encountered
	 *  first a month or a date of the month for date where it may not be obvious which
	 *  comes first.
     *  
     *  @see mx.formatters.DateBase
     * 
     *  @param str Date that is formatted as a String. 
     *
     *  @return Date object.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Royale 0.9.3
     */
    public static function parseDateString(str:String, format:String = null):Date
    {
        if (!str || str == "")
            return null;

        var year:int = -1;
        var mon:int = -1;
        var day:int = -1;
        var hour:int = -1;
        var min:int = -1;
        var sec:int = -1;
		var milli:int = -1;
        
        var letter:String = "";
        var marker:Object = 0;
        
        var count:int = 0;
        var len:int = 0;
		var isPM:Boolean = false;
		
		var punctuation:Object = {};
		var ampm:Object = {};
			
		punctuation["/"] = {general:true, date:true, time:false};
		punctuation[":"] = {general:true, date:false, time:true};
		punctuation[" "] = {general:true, date:true, time:false};
		punctuation["."] = {general:true, date:true, time:true};
		punctuation[","] = {general:true, date:true, time:false};
		punctuation["+"] = {general:true, date:false, time:false};
		punctuation["-"] = {general:true, date:true, time:false};
		// Chinese and Japanese
		punctuation["年"] = {general:true, date:true, time:false};
		punctuation["月"] = {general:true, date:true, time:false};
		punctuation["日"] = {general:true, date:true, time:false};
		punctuation["午"] = {general:false, date:false, time:true};
		// Korean
		punctuation["년"] = {general:true, date:true, time:false};
		punctuation["월"] = {general:true, date:true, time:false};
		punctuation["일"] = {general:true, date:true, time:false};
		
		ampm["PM"] = true;
		ampm["pm"] = true;
		ampm["\u33D8"] = true; // unicode pm
		ampm["μμ"] = true; // Greek
		ampm["午後"] = true; // Japanese
		ampm["上午"] = true; // Chinese
		ampm["오후"] = true; // Korean
		
        // Strip out the Timezone. It is not used by the DateFormatter
        var timezoneRegEx:RegExp = /(GMT|UTC)((\+|-)\d\d\d\d )?/ig;
        
        str = str.replace(timezoneRegEx, "");
		len = str.length;
        
        while (count < len)
        {
            letter = str.charAt(count);
            count++;

            // If the letter is a blank space or a comma,
            // continue to the next character
            if (letter <= " " || letter == ",")
                continue;

            // If the letter is a key punctuation character,
            // cache it for the next time around.
            if (punctuation.hasOwnProperty(letter) && punctuation[letter].general)
            {
                marker = letter;
                continue;
            }

            // Scan for groups of numbers and letters
            // and match them to Date parameters
            if (!("0" <= letter && letter <= "9" ||
				punctuation.hasOwnProperty(letter) && punctuation[letter].general))
            {
                // Scan for groups of letters
                var word:String = letter;
                while (count < len) 
                {
                    letter = str.charAt(count);
					if ("0" <= letter && letter <= "9" ||
						punctuation.hasOwnProperty(letter) && punctuation[letter].general)
                    {
                        break;
                    }
                    word += letter;
                    count++;
                }
				
                // Allow for an exact match
                // or a match to the first 3 letters as a prefix.
                var n:int = DateBase.defaultStringKey.length;
                for (var i:int = 0; i < n; i++)
                {
                    var s:String = String(DateBase.defaultStringKey[i]);
                    if (s.toLowerCase() == word.toLowerCase() ||
                        s.toLowerCase().substr(0,3) == word.toLowerCase())
                    {
                        if (i == 13) 
                        {
                            // pm
                            if (hour > 12 || hour < 1)
                                break; // error
                            else if (hour < 12)
                                hour += 12;
                        } 
                        else if (i == 12) 
                        {
                            // am
                            if (hour > 12 || hour < 1)
                                break; // error
                            else if (hour == 12)
                                hour = 0;

                        } 
                        else if (i < 12) 
                        {
                            // month
                            if (mon < 0)
                                mon = i;
                            else
                                break; // error
                        }
                        break;
                    }
                }
                marker = 0;
				
				// All known locales AM/PM 
				if (ampm.hasOwnProperty(word))
				{
					isPM = true;
					
					if (hour > 12)
						break; // error
					else if (hour >= 0 && hour < 12)
						hour += 12;
				}
            }
            
            else if ("0" <= letter && letter <= "9")
            {
                // Scan for groups of numbers
                var numbers:String = letter;
                while ("0" <= (letter = str.charAt(count)) &&
                       letter <= "9" &&
                       count < len)
                {
                    numbers += letter;
                    count++;
                }
                var num:int = int(numbers);

                // If num is a number greater than 70, assign num to year.
				// if after seconds and a dot or colon more likly milliseconds
                if (num >= 70 && !(punctuation.hasOwnProperty(letter)
					&& punctuation[letter].time && sec >= 0))
                {
                    if (year != -1)
                    {
                        break; // error
                    }
                    else if (punctuation.hasOwnProperty(letter)
						&& punctuation[letter].date
						|| count >= len)
                    {
                        year = num;
                    }
                    else
                    {
                        break; //error
                    }
                }

                // If the current letter is a slash or a dash,
                // assign num to year or month or day or min or sec.
                else if (punctuation.hasOwnProperty(letter) && punctuation[letter].date)
                {
					var monthFirst:Boolean = year != -1;
					
					if (format)
						monthFirst = monthFirst || format.search("M") < format.search("D");
							
					if (monthFirst && mon < 0)
                        mon = (num - 1);
					else if (day < 0)
						day = num;
					else if (!monthFirst && mon < 0)
						mon = (num -1);
					else if (min < 0)
						min = num;
					else if (sec < 0)
						sec = num;
					else if (milli < 0)
						milli = num;
                    else
                        break; //error
                }

                // If the current letter is a colon or a dot,
                // assign num to hour or minute or sec.
                else if (punctuation.hasOwnProperty(letter) && punctuation[letter].time)
                {
                    if (hour < 0)
					{
                        hour = num;
						
						if (isPM)
						{
							if (hour > 12)
								break; //error
							else if (hour >= 0 && hour < 12)
								hour += 12;
						}
					}
                    else if (min < 0)
                        min = num;
					else if (sec < 0)
						sec = num;
					else if (milli < 0)
						milli = num;
                    else
                        break; //error
                }

                // If hours are defined and minutes are not,
                // assign num to minutes.
                else if (hour >= 0 && min < 0)
                {
                    min = num;
                }

				// If minutes are defined and seconds are not,
				// assign num to seconds.
				else if (min >= 0 && sec < 0)
				{
					sec = num;
				}
				
				// If seconds are defined and millis are not,
				// assign num to mills.
				else if (sec >= 0 && milli < 0)
				{
					milli = num;
				}

                // If day is not defined, assign num to day.
                else if (day < 0)
                {
                    day = num;
                }

                // If month and day are defined and year is not,
                // assign num to year.
                else if (year < 0 && mon >= 0 && day >= 0)
                {
                    year = 2000 + num;
                }

                // Otherwise, break the loop
                else
                {
                    break;  //error
                }
                
                marker = 0;
            }
        }
		
        if (year < 0 || mon < 0 || mon > 11 || day < 1 || day > 31)
            return null; // error - needs to be a date

        // Time is set to 0 if null.
		if (milli < 0)
			milli = 0;
        if (sec < 0)
            sec = 0;
        if (min < 0)
            min = 0;
        if (hour < 0)
            hour = 0;

        // create a date object and check the validity of the input date
        // by comparing the result with input values.
        var newDate:Date = new Date(year, mon, day, hour, min, sec, milli);
        if (day != newDate.getDate() || mon != newDate.getMonth())
            return null;

        return newDate;
    }

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor.

  	 * 	@param formatString Date format pattern is set to this DateFormatter.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Royale 0.9.3
     */
    public function DateFormatter(formatString:String = null)
    {
        super();

        this.formatString = formatString;
	}

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  formatString
    //----------------------------------
    
    /**
     *  @private
     *  Storage for the formatString property.
     */
    private var _formatString:String;
    
    /**
     *  @private
     */
  /*   private var formatStringOverride:String;

    [Inspectable(category="General", defaultValue="null")] */

    /**
     *  The mask pattern.
     *  
     *  <p>You compose a pattern String using specific uppercase letters,
     *  for example: YYYY/MM.</p>
     *
     *  <p>The DateFormatter pattern String can contain other text
     *  in addition to pattern letters.
     *  To form a valid pattern String, you only need one pattern letter.</p>
     *      
     *  <p>The following table describes the valid pattern letters:</p>
     *
     *  <table class="innertable">
     *    <tr><th>Pattern letter</th><th>Description</th></tr>
     *    <tr>
     *      <td>Y</td>
     *      <td> Year. If the number of pattern letters is two, the year is 
     *        truncated to two digits; otherwise, it appears as four digits. 
     *        The year can be zero-padded, as the third example shows in the 
     *        following set of examples: 
     *        <ul>
     *          <li>YY = 05</li>
     *          <li>YYYY = 2005</li>
     *          <li>YYYYY = 02005</li>
     *        </ul></td>
     *    </tr>
     *    <tr>
     *      <td>M</td>
     *      <td> Month in year. The format depends on the following criteria:
     *        <ul>
     *          <li>If the number of pattern letters is one, the format is 
     *            interpreted as numeric in one or two digits. </li>
     *          <li>If the number of pattern letters is two, the format 
     *            is interpreted as numeric in two digits.</li>
     *          <li>If the number of pattern letters is three, 
     *            the format is interpreted as short text.</li>
     *          <li>If the number of pattern letters is four, the format 
     *           is interpreted as full text. </li>
     *        </ul>
     *          Examples:
     *        <ul>
     *          <li>M = 7</li>
     *          <li>MM= 07</li>
     *          <li>MMM=Jul</li>
     *          <li>MMMM= July</li>
     *        </ul></td>
     *    </tr>
     *    <tr>
     *      <td>D</td>
     *      <td>Day in month. While a single-letter pattern string for day is valid, 
     *        you typically use a two-letter pattern string.
     * 
     *        <p>Examples:</p>
     *        <ul>
     *          <li>D=4</li>
     *          <li>DD=04</li>
     *          <li>DD=10</li>
     *        </ul></td>
     *    </tr>
     *    <tr>
     *      <td>E</td>
     *      <td>Day in week. The format depends on the following criteria:
     *        <ul>
     *          <li>If the number of pattern letters is one, the format is 
     *            interpreted as numeric in one or two digits.</li>
     *          <li>If the number of pattern letters is two, the format is interpreted 
     *           as numeric in two digits.</li>
     *          <li>If the number of pattern letters is three, the format is interpreted 
     *            as short text. </li>
     *          <li>If the number of pattern letters is four, the format is interpreted 
     *           as full text. </li>
     *        </ul>
     *          Examples:
     *        <ul>
     *          <li>E = 1</li>
     *          <li>EE = 01</li>
     *          <li>EEE = Mon</li>
     *          <li>EEEE = Monday</li>
     *        </ul></td>
     *    </tr>
     *    <tr>
     *      <td>A</td>
     *      <td> am/pm indicator.</td>
     *    </tr>
     *    <tr>
     *      <td>J</td>
     *      <td>Hour in day (0-23).</td>
     *    </tr>
     *    <tr>
     *      <td>H</td>
     *      <td>Hour in day (1-24).</td>
     *    </tr>
     *    <tr>
     *      <td>K</td>
     *      <td>Hour in am/pm (0-11).</td>
     *    </tr>
     *    <tr>
     *      <td>L</td>
     *      <td>Hour in am/pm (1-12).</td>
     *    </tr>
     *    <tr>
     *      <td>N</td>
     *      <td>Minute in hour.
     * 
     *        <p>Examples:</p>
     *        <ul>
     *          <li>N = 3</li>
     *          <li>NN = 03</li>
     *        </ul></td>
     *    </tr>
     *    <tr>
     *      <td>S</td>
     *      <td>Second in minute. 
     * 
     *        <p>Example:</p>
     *        <ul>
     *          <li>SS = 30</li>
     *        </ul></td>
     *    </tr>
     *    <tr>
     *      <td>Q</td>
     *      <td>Millisecond in second
     * 
     *        <p>Example:</p>
     *        <ul>
     *          <li>QQ = 78</li>
     *          <li>QQQ = 078</li>
     *        </ul></td>
     *    </tr>
	 *    <tr>
     *      <td>O</td>
     *      <td>Timezone offset
     * 
     *        <p>Example:</p>
     *        <ul>
     *          <li>O = +7</li>
     *          <li>OO = -08</li>
	 *          <li>OOO = +4:30</li>
	 *          <li>OOOO = -08:30</li>
     *        </ul></td>
     *    </tr>
	 *    <tr>
     *      <td>Z</td>
     *      <td>Timezone name
     * 
     *        <p>Example:</p>
     *        <ul>
     *          <li>Z = GMT</li>
     *        </ul></td>
     *    </tr>
     *    <tr>
     *      <td>Other text</td>
     *      <td>You can add other text into the pattern string to further 
     *        format the string. You can use punctuation, numbers, 
     *        and all lowercase letters. You should avoid uppercase letters 
     *        because they may be interpreted as pattern letters.
     * 
     *        <p>Example:</p>
     *        <ul>
     *          <li>EEEE, MMM. D, YYYY at L:NN:QQQ A = Tuesday, Sept. 8, 2005 at 1:26:012 PM</li>
     *        </ul></td>
     *    </tr>
     *  </table>
     *
     *  @default "MM/DD/YYYY"
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Royale 0.9.3
     */
    public function get formatString():String
    {
        return _formatString;
    }

    /**
     *  @private
     */
    public function set formatString(value:String):void
    {
        //formatStringOverride = value;

        _formatString = value != null ?
                        value :
                        /*resourceManager.getString(
                            "SharedResources", "dateFormat");*/ "MM/DD/YYYY";
    }

    //--------------------------------------------------------------------------
    //
    //  Overridden methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private    
     */
    /* override protected function resourcesChanged():void
    {
        super.resourcesChanged();

        formatString = formatStringOverride;
    } */
    
    /**
     *  Generates a date-formatted String from either a date-formatted String or a Date object. 
     *  The <code>formatString</code> property
     *  determines the format of the output String.
     *  If <code>value</code> cannot be formatted, return an empty String 
     *  and write a description of the error to the <code>error</code> property.
     *
     *  @param value Date to format. This can be a Date object,
     *  or a date-formatted String such as "Thursday, April 22, 2004".
     *
     *  @return Formatted String. Empty if an error occurs. A description 
     *  of the error condition is written to the <code>error</code> property.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Royale 0.9.3
     */
    override public function format(value:Object):String
    {       
        // Reset any previous errors.
        if (error)
            error = null;

        // If value is null, or empty String just return "" 
        // but treat it as an error for consistency.
        // Users will ignore it anyway.
        if (!value || (value is String && value == ""))        
        {
            error = defaultInvalidValueError;
            return "";
        }

        // -- value --

        if (value is String)
        {
            value = DateFormatter.parseDateString(String(value), formatString);
            if (!value)
            {
                error = defaultInvalidValueError;
                return "";
			}
        }
        else if (!(value is Date))
        {
            error = defaultInvalidValueError;
            return "";
        }

        // -- format --

        var letter:String;
        var nTokens:int = 0;
        var tokens:String = "";
        
        var n:int = formatString.length;
        for (var i:int = 0; i < n; i++)
        {
            letter = formatString.charAt(i);
            if (VALID_PATTERN_CHARS.indexOf(letter) != -1 && letter != ",")
            {
                nTokens++;
                if (tokens.indexOf(letter) == -1)
                {
                    tokens += letter;
                }
                else
                {
                    if (letter != formatString.charAt(Math.max(i - 1, 0)))
                    {
                        error = defaultInvalidFormatError;
                        return "";
                    }
                }
            }
        }

        if (nTokens < 1)
        {
            error = defaultInvalidFormatError;
            return "";
        }

        var dataFormatter:StringFormatter = new StringFormatter(
            formatString, VALID_PATTERN_CHARS,
            DateBase.extractTokenDate);

        return dataFormatter.formatValue(value);
    }
}

}
