// Package dateparse parses date-strings without knowing the format
// in advance, using a fast lex based approach to eliminate shotgun
// attempts.  It leans towards US style dates when there is a conflict.
package dateparse

import (
	"fmt"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"
)

// func init() {
// 	gou.SetupLogging("debug")
// 	gou.SetColorOutput()
// }

var days = []string{
	"mon",
	"tue",
	"wed",
	"thu",
	"fri",
	"sat",
	"sun",
	"monday",
	"tuesday",
	"wednesday",
	"thursday",
	"friday",
	"saturday",
	"sunday",
}

var months = []string{
	"january",
	"february",
	"march",
	"april",
	"may",
	"june",
	"july",
	"august",
	"september",
	"october",
	"november",
	"december",
}

type dateState uint8
type timeState uint8

const (
	dateStart dateState = iota // 0
	dateDigit
	dateDigitSt
	dateYearDash
	dateYearDashAlphaDash
	dateYearDashDash
	dateYearDashDashWs // 5
	dateYearDashDashT
	dateYearDashDashOffset
	dateDigitDash
	dateDigitDashAlpha
	dateDigitDashAlphaDash // 10
	dateDigitDot
	dateDigitDotDot
	dateDigitSlash
	dateDigitYearSlash
	dateDigitSlashAlpha // 15
	dateDigitColon
	dateDigitChineseYear
	dateDigitChineseYearWs
	dateDigitWs
	dateDigitWsMoYear // 20
	dateDigitWsMolong
	dateAlpha
	dateAlphaWs
	dateAlphaWsDigit
	dateAlphaWsDigitMore // 25
	dateAlphaWsDigitMoreWs
	dateAlphaWsDigitMoreWsYear
	dateAlphaWsMonth
	dateAlphaWsDigitYearmaybe
	dateAlphaWsMonthMore
	dateAlphaWsMonthSuffix
	dateAlphaWsMore
	dateAlphaWsAtTime
	dateAlphaWsAlpha
	dateAlphaWsAlphaYearmaybe // 35
	dateAlphaPeriodWsDigit
	dateWeekdayComma
	dateWeekdayAbbrevComma
)
const (
	// Time state
	timeIgnore timeState = iota // 0
	timeStart
	timeWs
	timeWsAlpha
	timeWsAlphaWs
	timeWsAlphaZoneOffset // 5
	timeWsAlphaZoneOffsetWs
	timeWsAlphaZoneOffsetWsYear
	timeWsAlphaZoneOffsetWsExtra
	timeWsAMPMMaybe
	timeWsAMPM // 10
	timeWsOffset
	timeWsOffsetWs // 12
	timeWsOffsetColonAlpha
	timeWsOffsetColon
	timeWsYear // 15
	timeOffset
	timeOffsetColon
	timeAlpha
	timePeriod
	timePeriodOffset // 20
	timePeriodOffsetColon
	timePeriodOffsetColonWs
	timePeriodWs
	timePeriodWsAlpha
	timePeriodWsOffset // 25
	timePeriodWsOffsetWs
	timePeriodWsOffsetWsAlpha
	timePeriodWsOffsetColon
	timePeriodWsOffsetColonAlpha
	timeZ
	timeZDigit
)

var (
	// ErrAmbiguousMMDD for date formats such as 04/02/2014 the mm/dd vs dd/mm are
	// ambiguous, so it is an error for strict parse rules.
	ErrAmbiguousMMDD = fmt.Errorf("This date has ambiguous mm/dd vs dd/mm type format")
)

func unknownErr(datestr string) error {
	return fmt.Errorf("Could not find format for %q", datestr)
}

// ParseAny parse an unknown date format, detect the layout.
// Normal parse.  Equivalent Timezone rules as time.Parse().
// NOTE:  please see readme on mmdd vs ddmm ambiguous dates.
func ParseAny(datestr string, opts ...ParserOption) (time.Time, error) {
	p, err := parseTime(datestr, nil, opts...)
	if err != nil {
		return time.Time{}, err
	}
	return p.parse()
}

// ParseIn with Location, equivalent to time.ParseInLocation() timezone/offset
// rules.  Using location arg, if timezone/offset info exists in the
// datestring, it uses the given location rules for any zone interpretation.
// That is, MST means one thing when using America/Denver and something else
// in other locations.
func ParseIn(datestr string, loc *time.Location, opts ...ParserOption) (time.Time, error) {
	p, err := parseTime(datestr, loc, opts...)
	if err != nil {
		return time.Time{}, err
	}
	return p.parse()
}

// ParseLocal Given an unknown date format, detect the layout,
// using time.Local, parse.
//
// Set Location to time.Local.  Same as ParseIn Location but lazily uses
// the global time.Local variable for Location argument.
//
//     denverLoc, _ := time.LoadLocation("America/Denver")
//     time.Local = denverLoc
//
//     t, err := dateparse.ParseLocal("3/1/2014")
//
// Equivalent to:
//
//     t, err := dateparse.ParseIn("3/1/2014", denverLoc)
//
func ParseLocal(datestr string, opts ...ParserOption) (time.Time, error) {
	p, err := parseTime(datestr, time.Local, opts...)
	if err != nil {
		return time.Time{}, err
	}
	return p.parse()
}

// MustParse  parse a date, and panic if it can't be parsed.  Used for testing.
// Not recommended for most use-cases.
func MustParse(datestr string, opts ...ParserOption) time.Time {
	p, err := parseTime(datestr, nil, opts...)
	if err != nil {
		panic(err.Error())
	}
	t, err := p.parse()
	if err != nil {
		panic(err.Error())
	}
	return t
}

// ParseFormat parse's an unknown date-time string and returns a layout
// string that can parse this (and exact same format) other date-time strings.
//
//     layout, err := dateparse.ParseFormat("2013-02-01 00:00:00")
//     // layout = "2006-01-02 15:04:05"
//
func ParseFormat(datestr string, opts ...ParserOption) (string, error) {
	p, err := parseTime(datestr, nil, opts...)
	if err != nil {
		return "", err
	}
	_, err = p.parse()
	if err != nil {
		return "", err
	}
	return string(p.format), nil
}

// ParseStrict parse an unknown date format.  IF the date is ambigous
// mm/dd vs dd/mm then return an error. These return errors:   3.3.2014 , 8/8/71 etc
func ParseStrict(datestr string, opts ...ParserOption) (time.Time, error) {
	p, err := parseTime(datestr, nil, opts...)
	if err != nil {
		return time.Time{}, err
	}
	if p.ambiguousMD {
		return time.Time{}, ErrAmbiguousMMDD
	}
	return p.parse()
}

func parseTime(datestr string, loc *time.Location, opts ...ParserOption) (p *parser, err error) {

	p = newParser(datestr, loc, opts...)
	if p.retryAmbiguousDateWithSwap {
		// month out of range signifies that a day/month swap is the correct solution to an ambiguous date
		// this is because it means that a day is being interpreted as a month and overflowing the valid value for that
		// by retrying in this case, we can fix a common situation with no assumptions
		defer func() {
			if p.ambiguousMD {
				// if it errors out with the following error, swap before we
				// get out of this function to reduce scope it needs to be applied on
				_, err := p.parse()
				if err != nil && strings.Contains(err.Error(), "month out of range") {
					// create the option to reverse the preference
					preferMonthFirst := PreferMonthFirst(!p.preferMonthFirst)
					// turn off the retry to avoid endless recursion
					retryAmbiguousDateWithSwap := RetryAmbiguousDateWithSwap(false)
					modifiedOpts := append(opts, preferMonthFirst, retryAmbiguousDateWithSwap)
					p, err = parseTime(datestr, time.Local, modifiedOpts...)
				}
			}

		}()
	}

	i := 0

	// General strategy is to read rune by rune through the date looking for
	// certain hints of what type of date we are dealing with.
	// Hopefully we only need to read about 5 or 6 bytes before
	// we figure it out and then attempt a parse
iterRunes:
	for ; i < len(datestr); i++ {
		//r := rune(datestr[i])
		r, bytesConsumed := utf8.DecodeRuneInString(datestr[i:])
		if bytesConsumed > 1 {
			i += (bytesConsumed - 1)
		}

		// gou.Debugf("i=%d r=%s state=%d   %s", i, string(r), p.stateDate, datestr)
		switch p.stateDate {
		case dateStart:
			if unicode.IsDigit(r) {
				p.stateDate = dateDigit
			} else if unicode.IsLetter(r) {
				p.stateDate = dateAlpha
			} else {
				return nil, unknownErr(datestr)
			}
		case dateDigit:

			switch r {
			case '-', '\u2212':
				// 2006-01-02
				// 2013-Feb-03
				// 13-Feb-03
				// 29-Jun-2016
				if i == 4 {
					p.stateDate = dateYearDash
					p.yeari = 0
					p.yearlen = i
					p.moi = i + 1
					p.set(0, "2006")
				} else {
					p.stateDate = dateDigitDash
				}
			case '/':
				// 08/May/2005
				// 03/31/2005
				// 2014/02/24
				p.stateDate = dateDigitSlash
				if i == 4 {
					// 2014/02/24  -  Year first /
					p.yearlen = i // since it was start of datestr, i=len
					p.moi = i + 1
					p.setYear()
					p.stateDate = dateDigitYearSlash
				} else {
					// Either Ambiguous dd/mm vs mm/dd  OR dd/month/yy
					// 08/May/2005
					// 03/31/2005
					// 31/03/2005
					if i+2 < len(p.datestr) && unicode.IsLetter(rune(datestr[i+1])) {
						// 08/May/2005
						p.stateDate = dateDigitSlashAlpha
						p.moi = i + 1
						p.daylen = 2
						p.dayi = 0
						p.setDay()
						continue
					}
					// Ambiguous dd/mm vs mm/dd the bane of date-parsing
					// 03/31/2005
					// 31/03/2005
					p.ambiguousMD = true
					if p.preferMonthFirst {
						if p.molen == 0 {
							// 03/31/2005
							p.molen = i
							p.setMonth()
							p.dayi = i + 1
						}
					} else {
						if p.daylen == 0 {
							p.daylen = i
							p.setDay()
							p.moi = i + 1
						}
					}

				}

			case ':':
				// 03/31/2005
				// 2014/02/24
				p.stateDate = dateDigitColon
				if i == 4 {
					p.yearlen = i
					p.moi = i + 1
					p.setYear()
				} else {
					p.ambiguousMD = true
					if p.preferMonthFirst {
						if p.molen == 0 {
							p.molen = i
							p.setMonth()
							p.dayi = i + 1
						}
					}
				}

			case '.':
				// 3.31.2014
				// 08.21.71
				// 2014.05
				p.stateDate = dateDigitDot
				if i == 4 {
					p.yearlen = i
					p.moi = i + 1
					p.setYear()
				} else {
					p.ambiguousMD = true
					p.moi = 0
					p.molen = i
					p.setMonth()
					p.dayi = i + 1
				}

			case ' ':
				// 18 January 2018
				// 8 January 2018
				// 8 jan 2018
				// 02 Jan 2018 23:59
				// 02 Jan 2018 23:59:34
				// 12 Feb 2006, 19:17
				// 12 Feb 2006, 19:17:22
				if i == 6 {
					p.stateDate = dateDigitSt
				} else {
					p.stateDate = dateDigitWs
					p.dayi = 0
					p.daylen = i
				}
			case '年':
				// Chinese Year
				p.stateDate = dateDigitChineseYear
			case ',':
				return nil, unknownErr(datestr)
			default:
				continue
			}
			p.part1Len = i

		case dateDigitSt:
			p.set(0, "060102")
			i = i - 1
			p.stateTime = timeStart
			break iterRunes
		case dateYearDash:
			// dateYearDashDashT
			//  2006-01-02T15:04:05Z07:00
			//  2020-08-17T17:00:00:000+0100
			// dateYearDashDashWs
			//  2013-04-01 22:43:22
			// dateYearDashAlphaDash
			//   2013-Feb-03
			switch r {
			case '-':
				p.molen = i - p.moi
				p.dayi = i + 1
				p.stateDate = dateYearDashDash
				p.setMonth()
			default:
				if unicode.IsLetter(r) {
					p.stateDate = dateYearDashAlphaDash
				}
			}

		case dateYearDashDash:
			// dateYearDashDashT
			//  2006-01-02T15:04:05Z07:00
			// dateYearDashDashWs
			//  2013-04-01 22:43:22
			// dateYearDashDashOffset
			//  2020-07-20+00:00
			switch r {
			case '+', '-':
				p.offseti = i
				p.daylen = i - p.dayi
				p.stateDate = dateYearDashDashOffset
				p.setDay()
			case ' ':
				p.daylen = i - p.dayi
				p.stateDate = dateYearDashDashWs
				p.stateTime = timeStart
				p.setDay()
				break iterRunes
			case 'T':
				p.daylen = i - p.dayi
				p.stateDate = dateYearDashDashT
				p.stateTime = timeStart
				p.setDay()
				break iterRunes
			}

		case dateYearDashDashT:
			// dateYearDashDashT
			//  2006-01-02T15:04:05Z07:00
			//  2020-08-17T17:00:00:000+0100

		case dateYearDashDashOffset:
			//  2020-07-20+00:00
			switch r {
			case ':':
				p.set(p.offseti, "-07:00")
				// case ' ':
				// 	return nil, unknownErr(datestr)
			}

		case dateYearDashAlphaDash:
			// 2013-Feb-03
			switch r {
			case '-':
				p.molen = i - p.moi
				p.set(p.moi, "Jan")
				p.dayi = i + 1
			}
		case dateDigitDash:
			// 13-Feb-03
			// 29-Jun-2016
			if unicode.IsLetter(r) {
				p.stateDate = dateDigitDashAlpha
				p.moi = i
			} else {
				return nil, unknownErr(datestr)
			}
		case dateDigitDashAlpha:
			// 13-Feb-03
			// 28-Feb-03
			// 29-Jun-2016
			switch r {
			case '-':
				p.molen = i - p.moi
				p.set(p.moi, "Jan")
				p.yeari = i + 1
				p.stateDate = dateDigitDashAlphaDash
			}

		case dateDigitDashAlphaDash:
			// 13-Feb-03   ambiguous
			// 28-Feb-03   ambiguous
			// 29-Jun-2016  dd-month(alpha)-yyyy
			switch r {
			case ' ':
				// we need to find if this was 4 digits, aka year
				// or 2 digits which makes it ambiguous year/day
				length := i - (p.moi + p.molen + 1)
				if length == 4 {
					p.yearlen = 4
					p.set(p.yeari, "2006")
					// We now also know that part1 was the day
					p.dayi = 0
					p.daylen = p.part1Len
					p.setDay()
				} else if length == 2 {
					// We have no idea if this is
					// yy-mon-dd   OR  dd-mon-yy
					//
					// We are going to ASSUME (bad, bad) that it is dd-mon-yy  which is a horible assumption
					p.ambiguousMD = true
					p.yearlen = 2
					p.set(p.yeari, "06")
					// We now also know that part1 was the day
					p.dayi = 0
					p.daylen = p.part1Len
					p.setDay()
				}
				p.stateTime = timeStart
				break iterRunes
			}

		case dateDigitYearSlash:
			// 2014/07/10 06:55:38.156283
			// I honestly don't know if this format ever shows up as yyyy/

			switch r {
			case ' ', ':':
				p.stateTime = timeStart
				if p.daylen == 0 {
					p.daylen = i - p.dayi
					p.setDay()
				}
				break iterRunes
			case '/':
				if p.molen == 0 {
					p.molen = i - p.moi
					p.setMonth()
					p.dayi = i + 1
				}
			}

		case dateDigitSlashAlpha:
			// 06/May/2008

			switch r {
			case '/':
				//       |
				// 06/May/2008
				if p.molen == 0 {
					p.set(p.moi, "Jan")
					p.yeari = i + 1
				}
				// We aren't breaking because we are going to re-use this case
				// to find where the date starts, and possible time begins
			case ' ', ':':
				p.stateTime = timeStart
				if p.yearlen == 0 {
					p.yearlen = i - p.yeari
					p.setYear()
				}
				break iterRunes
			}

		case dateDigitSlash:
			// 03/19/2012 10:11:59
			// 04/2/2014 03:00:37
			// 3/1/2012 10:11:59
			// 4/8/2014 22:05
			// 3/1/2014
			// 10/13/2014
			// 01/02/2006
			// 1/2/06

			switch r {
			case '/':
				// This is the 2nd / so now we should know start pts of all of the dd, mm, yy
				if p.preferMonthFirst {
					if p.daylen == 0 {
						p.daylen = i - p.dayi
						p.setDay()
						p.yeari = i + 1
					}
				} else {
					if p.molen == 0 {
						p.molen = i - p.moi
						p.setMonth()
						p.yeari = i + 1
					}
				}
				// Note no break, we are going to pass by and re-enter this dateDigitSlash
				// and look for ending (space) or not (just date)
			case ' ':
				p.stateTime = timeStart
				if p.yearlen == 0 {
					p.yearlen = i - p.yeari
					p.setYear()
				}
				break iterRunes
			}

		case dateDigitColon:
			// 2014:07:10 06:55:38.156283
			// 03:19:2012 10:11:59
			// 04:2:2014 03:00:37
			// 3:1:2012 10:11:59
			// 4:8:2014 22:05
			// 3:1:2014
			// 10:13:2014
			// 01:02:2006
			// 1:2:06

			switch r {
			case ' ':
				p.stateTime = timeStart
				if p.yearlen == 0 {
					p.yearlen = i - p.yeari
					p.setYear()
				} else if p.daylen == 0 {
					p.daylen = i - p.dayi
					p.setDay()
				}
				break iterRunes
			case ':':
				if p.yearlen > 0 {
					// 2014:07:10 06:55:38.156283
					if p.molen == 0 {
						p.molen = i - p.moi
						p.setMonth()
						p.dayi = i + 1
					}
				} else if p.preferMonthFirst {
					if p.daylen == 0 {
						p.daylen = i - p.dayi
						p.setDay()
						p.yeari = i + 1
					}
				}
			}

		case dateDigitWs:
			// 18 January 2018
			// 8 January 2018
			// 8 jan 2018
			// 1 jan 18
			// 02 Jan 2018 23:59
			// 02 Jan 2018 23:59:34
			// 12 Feb 2006, 19:17
			// 12 Feb 2006, 19:17:22
			switch r {
			case ' ':
				p.yeari = i + 1
				//p.yearlen = 4
				p.dayi = 0
				p.daylen = p.part1Len
				p.setDay()
				p.stateTime = timeStart
				if i > p.daylen+len(" Sep") { //  November etc
					// If len greather than space + 3 it must be full month
					p.stateDate = dateDigitWsMolong
				} else {
					// If len=3, the might be Feb or May?  Ie ambigous abbreviated but
					// we can parse may with either.  BUT, that means the
					// format may not be correct?
					// mo := strings.ToLower(datestr[p.daylen+1 : i])
					p.moi = p.daylen + 1
					p.molen = i - p.moi
					p.set(p.moi, "Jan")
					p.stateDate = dateDigitWsMoYear
				}
			}

		case dateDigitWsMoYear:
			// 8 jan 2018
			// 02 Jan 2018 23:59
			// 02 Jan 2018 23:59:34
			// 12 Feb 2006, 19:17
			// 12 Feb 2006, 19:17:22
			switch r {
			case ',':
				p.yearlen = i - p.yeari
				p.setYear()
				i++
				break iterRunes
			case ' ':
				p.yearlen = i - p.yeari
				p.setYear()
				break iterRunes
			}
		case dateDigitWsMolong:
			// 18 January 2018
			// 8 January 2018

		case dateDigitChineseYear:
			// dateDigitChineseYear
			//   2014年04月08日
			//               weekday  %Y年%m月%e日 %A %I:%M %p
			// 2013年07月18日 星期四 10:27 上午
			if r == ' ' {
				p.stateDate = dateDigitChineseYearWs
				break
			}
		case dateDigitDot:
			// This is the 2nd period
			// 3.31.2014
			// 08.21.71
			// 2014.05
			// 2018.09.30
			if r == '.' {
				if p.moi == 0 {
					// 3.31.2014
					p.daylen = i - p.dayi
					p.yeari = i + 1
					p.setDay()
					p.stateDate = dateDigitDotDot
				} else {
					// 2018.09.30
					//p.molen = 2
					p.molen = i - p.moi
					p.dayi = i + 1
					p.setMonth()
					p.stateDate = dateDigitDotDot
				}
			}
		case dateDigitDotDot:
			// iterate all the way through
		case dateAlpha:
			// dateAlphaWS
			//  Mon Jan _2 15:04:05 2006
			//  Mon Jan _2 15:04:05 MST 2006
			//  Mon Jan 02 15:04:05 -0700 2006
			//  Mon Aug 10 15:44:11 UTC+0100 2015
			//  Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)
			//  dateAlphaWSDigit
			//    May 8, 2009 5:57:51 PM
			//    oct 1, 1970
			//  dateAlphaWsMonth
			//    April 8, 2009
			//  dateAlphaWsMore
			//    dateAlphaWsAtTime
			//      January 02, 2006 at 3:04pm MST-07
			//
			//  dateAlphaPeriodWsDigit
			//    oct. 1, 1970
			// dateWeekdayComma
			//   Monday, 02 Jan 2006 15:04:05 MST
			//   Monday, 02-Jan-06 15:04:05 MST
			//   Monday, 02 Jan 2006 15:04:05 -0700
			//   Monday, 02 Jan 2006 15:04:05 +0100
			// dateWeekdayAbbrevComma
			//   Mon, 02 Jan 2006 15:04:05 MST
			//   Mon, 02 Jan 2006 15:04:05 -0700
			//   Thu, 13 Jul 2017 08:58:40 +0100
			//   Tue, 11 Jul 2017 16:28:13 +0200 (CEST)
			//   Mon, 02-Jan-06 15:04:05 MST
			switch {
			case r == ' ':
				//      X
				// April 8, 2009
				if i > 3 {
					// Check to see if the alpha is name of month?  or Day?
					month := strings.ToLower(datestr[0:i])
					if isMonthFull(month) {
						p.fullMonth = month
						// len(" 31, 2018")   = 9
						if len(datestr[i:]) < 10 {
							// April 8, 2009
							p.stateDate = dateAlphaWsMonth
						} else {
							p.stateDate = dateAlphaWsMore
						}
						p.dayi = i + 1
						break
					}

				} else {
					// This is possibly ambiguous?  May will parse as either though.
					// So, it could return in-correct format.
					// dateAlphaWs
					//   May 05, 2005, 05:05:05
					//   May 05 2005, 05:05:05
					//   Jul 05, 2005, 05:05:05
					//   May 8 17:57:51 2009
					//   May  8 17:57:51 2009
					// skip & return to dateStart
					//   Tue 05 May 2020, 05:05:05
					//   Mon Jan  2 15:04:05 2006

					maybeDay := strings.ToLower(datestr[0:i])
					if isDay(maybeDay) {
						// using skip throws off indices used by other code; saner to restart
						return parseTime(datestr[i+1:], loc)
					}
					p.stateDate = dateAlphaWs
				}

			case r == ',':
				// Mon, 02 Jan 2006

				if i == 3 {
					p.stateDate = dateWeekdayAbbrevComma
					p.set(0, "Mon")
				} else {
					p.stateDate = dateWeekdayComma
					p.skip = i + 2
					i++
					// TODO:  lets just make this "skip" as we don't need
					// the mon, monday, they are all superfelous and not needed
					// just lay down the skip, no need to fill and then skip
				}
			case r == '.':
				// sept. 28, 2017
				// jan. 28, 2017
				p.stateDate = dateAlphaPeriodWsDigit
				if i == 3 {
					p.molen = i
					p.set(0, "Jan")
				} else if i == 4 {
					// gross
					datestr = datestr[0:i-1] + datestr[i:]
					return parseTime(datestr, loc, opts...)
				} else {
					return nil, unknownErr(datestr)
				}
			}

		case dateAlphaWs:
			// dateAlphaWsAlpha
			//   Mon Jan _2 15:04:05 2006
			//   Mon Jan _2 15:04:05 MST 2006
			//   Mon Jan 02 15:04:05 -0700 2006
			//   Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)
			//   Mon Aug 10 15:44:11 UTC+0100 2015
			// dateAlphaWsDigit
			//   May 8, 2009 5:57:51 PM
			//   May 8 2009 5:57:51 PM
			//   May 8 17:57:51 2009
			//   May  8 17:57:51 2009
			//   May 08 17:57:51 2009
			//   oct 1, 1970
			//   oct 7, '70
			switch {
			case unicode.IsLetter(r):
				p.set(0, "Mon")
				p.stateDate = dateAlphaWsAlpha
				p.set(i, "Jan")
			case unicode.IsDigit(r):
				p.set(0, "Jan")
				p.stateDate = dateAlphaWsDigit
				p.dayi = i
			}

		case dateAlphaWsDigit:
			// May 8, 2009 5:57:51 PM
			// May 8 2009 5:57:51 PM
			// oct 1, 1970
			// oct 7, '70
			// oct. 7, 1970
			// May 8 17:57:51 2009
			// May  8 17:57:51 2009
			// May 08 17:57:51 2009
			if r == ',' {
				p.daylen = i - p.dayi
				p.setDay()
				p.stateDate = dateAlphaWsDigitMore
			} else if r == ' ' {
				p.daylen = i - p.dayi
				p.setDay()
				p.yeari = i + 1
				p.stateDate = dateAlphaWsDigitYearmaybe
				p.stateTime = timeStart
			} else if unicode.IsLetter(r) {
				p.stateDate = dateAlphaWsMonthSuffix
				i--
			}
		case dateAlphaWsDigitYearmaybe:
			//       x
			// May 8 2009 5:57:51 PM
			// May 8 17:57:51 2009
			// May  8 17:57:51 2009
			// May 08 17:57:51 2009
			// Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)
			if r == ':' {
				// Guessed wrong; was not a year
				i = i - 3
				p.stateDate = dateAlphaWsDigit
				p.yeari = 0
				break iterRunes
			} else if r == ' ' {
				// must be year format, not 15:04
				p.yearlen = i - p.yeari
				p.setYear()
				break iterRunes
			}
		case dateAlphaWsDigitMore:
			//       x
			// May 8, 2009 5:57:51 PM
			// May 05, 2005, 05:05:05
			// May 05 2005, 05:05:05
			// oct 1, 1970
			// oct 7, '70
			if r == ' ' {
				p.yeari = i + 1
				p.stateDate = dateAlphaWsDigitMoreWs
			}
		case dateAlphaWsDigitMoreWs:
			//            x
			// May 8, 2009 5:57:51 PM
			// May 05, 2005, 05:05:05
			// oct 1, 1970
			// oct 7, '70
			switch r {
			case '\'':
				p.yeari = i + 1
			case ' ', ',':
				//            x
				// May 8, 2009 5:57:51 PM
				//            x
				// May 8, 2009, 5:57:51 PM
				p.stateDate = dateAlphaWsDigitMoreWsYear
				p.yearlen = i - p.yeari
				p.setYear()
				p.stateTime = timeStart
				break iterRunes
			}

		case dateAlphaWsMonth:
			// April 8, 2009
			// April 8 2009
			switch r {
			case ' ', ',':
				//       x
				// June 8, 2009
				//       x
				// June 8 2009
				if p.daylen == 0 {
					p.daylen = i - p.dayi
					p.setDay()
				}
			case 's', 'S', 'r', 'R', 't', 'T', 'n', 'N':
				// st, rd, nd, st
				i--
				p.stateDate = dateAlphaWsMonthSuffix
			default:
				if p.daylen > 0 && p.yeari == 0 {
					p.yeari = i
				}
			}
		case dateAlphaWsMonthMore:
			//                  X
			// January 02, 2006, 15:04:05
			// January 02 2006, 15:04:05
			// January 02, 2006 15:04:05
			// January 02 2006 15:04:05
			switch r {
			case ',':
				p.yearlen = i - p.yeari
				p.setYear()
				p.stateTime = timeStart
				i++
				break iterRunes
			case ' ':
				p.yearlen = i - p.yeari
				p.setYear()
				p.stateTime = timeStart
				break iterRunes
			}
		case dateAlphaWsMonthSuffix:
			//        x
			// April 8th, 2009
			// April 8th 2009
			switch r {
			case 't', 'T':
				if p.nextIs(i, 'h') || p.nextIs(i, 'H') {
					if len(datestr) > i+2 {
						return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...)
					}
				}
			case 'n', 'N':
				if p.nextIs(i, 'd') || p.nextIs(i, 'D') {
					if len(datestr) > i+2 {
						return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...)
					}
				}
			case 's', 'S':
				if p.nextIs(i, 't') || p.nextIs(i, 'T') {
					if len(datestr) > i+2 {
						return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...)
					}
				}
			case 'r', 'R':
				if p.nextIs(i, 'd') || p.nextIs(i, 'D') {
					if len(datestr) > i+2 {
						return parseTime(fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+2:]), loc, opts...)
					}
				}
			}
		case dateAlphaWsMore:
			// January 02, 2006, 15:04:05
			// January 02 2006, 15:04:05
			// January 2nd, 2006, 15:04:05
			// January 2nd 2006, 15:04:05
			// September 17, 2012 at 5:00pm UTC-05
			switch {
			case r == ',':
				//           x
				// January 02, 2006, 15:04:05
				if p.nextIs(i, ' ') {
					p.daylen = i - p.dayi
					p.setDay()
					p.yeari = i + 2
					p.stateDate = dateAlphaWsMonthMore
					i++
				}

			case r == ' ':
				//           x
				// January 02 2006, 15:04:05
				p.daylen = i - p.dayi
				p.setDay()
				p.yeari = i + 1
				p.stateDate = dateAlphaWsMonthMore
			case unicode.IsDigit(r):
				//         XX
				// January 02, 2006, 15:04:05
				continue
			case unicode.IsLetter(r):
				//          X
				// January 2nd, 2006, 15:04:05
				p.daylen = i - p.dayi
				p.setDay()
				p.stateDate = dateAlphaWsMonthSuffix
				i--
			}

		case dateAlphaPeriodWsDigit:
			//    oct. 7, '70
			switch {
			case r == ' ':
				// continue
			case unicode.IsDigit(r):
				p.stateDate = dateAlphaWsDigit
				p.dayi = i
			default:
				return p, unknownErr(datestr)
			}
		case dateWeekdayComma:
			// Monday, 02 Jan 2006 15:04:05 MST
			// Monday, 02 Jan 2006 15:04:05 -0700
			// Monday, 02 Jan 2006 15:04:05 +0100
			// Monday, 02-Jan-06 15:04:05 MST
			if p.dayi == 0 {
				p.dayi = i
			}
			switch r {
			case ' ', '-':
				if p.moi == 0 {
					p.moi = i + 1
					p.daylen = i - p.dayi
					p.setDay()
				} else if p.yeari == 0 {
					p.yeari = i + 1
					p.molen = i - p.moi
					p.set(p.moi, "Jan")
				} else {
					p.stateTime = timeStart
					break iterRunes
				}
			}
		case dateWeekdayAbbrevComma:
			// Mon, 02 Jan 2006 15:04:05 MST
			// Mon, 02 Jan 2006 15:04:05 -0700
			// Thu, 13 Jul 2017 08:58:40 +0100
			// Thu, 4 Jan 2018 17:53:36 +0000
			// Tue, 11 Jul 2017 16:28:13 +0200 (CEST)
			// Mon, 02-Jan-06 15:04:05 MST
			switch r {
			case ' ', '-':
				if p.dayi == 0 {
					p.dayi = i + 1
				} else if p.moi == 0 {
					p.daylen = i - p.dayi
					p.setDay()
					p.moi = i + 1
				} else if p.yeari == 0 {
					p.molen = i - p.moi
					p.set(p.moi, "Jan")
					p.yeari = i + 1
				} else {
					p.yearlen = i - p.yeari
					p.setYear()
					p.stateTime = timeStart
					break iterRunes
				}
			}

		default:
			break iterRunes
		}
	}
	p.coalesceDate(i)
	if p.stateTime == timeStart {
		// increment first one, since the i++ occurs at end of loop
		if i < len(p.datestr) {
			i++
		}
		// ensure we skip any whitespace prefix
		for ; i < len(datestr); i++ {
			r := rune(datestr[i])
			if r != ' ' {
				break
			}
		}

	iterTimeRunes:
		for ; i < len(datestr); i++ {
			r := rune(datestr[i])

			// gou.Debugf("i=%d r=%s state=%d iterTimeRunes  %s %s", i, string(r), p.stateTime, p.ds(), p.ts())

			switch p.stateTime {
			case timeStart:
				// 22:43:22
				// 22:43
				// timeComma
				//   08:20:13,787
				// timeWs
				//   05:24:37 PM
				//   06:20:00 UTC
				//   06:20:00 UTC-05
				//   00:12:00 +0000 UTC
				//   22:18:00 +0000 UTC m=+0.000000001
				//   15:04:05 -0700
				//   15:04:05 -07:00
				//   15:04:05 2008
				// timeOffset
				//   03:21:51+00:00
				//   19:55:00+0100
				// timePeriod
				//   17:24:37.3186369
				//   00:07:31.945167
				//   18:31:59.257000000
				//   00:00:00.000
				//   timePeriodOffset
				//     19:55:00.799+0100
				//     timePeriodOffsetColon
				//       15:04:05.999-07:00
				//   timePeriodWs
				//     timePeriodWsOffset
				//       00:07:31.945167 +0000
				//       00:00:00.000 +0000
				//     timePeriodWsOffsetAlpha
				//       00:07:31.945167 +0000 UTC
				//       22:18:00.001 +0000 UTC m=+0.000000001
				//       00:00:00.000 +0000 UTC
				//     timePeriodWsAlpha
				//       06:20:00.000 UTC
				if p.houri == 0 {
					p.houri = i
				}
				switch r {
				case ',':
					// hm, lets just swap out comma for period.  for some reason go
					// won't parse it.
					// 2014-05-11 08:20:13,787
					ds := []byte(p.datestr)
					ds[i] = '.'
					return parseTime(string(ds), loc, opts...)
				case '-', '+':
					//   03:21:51+00:00
					p.stateTime = timeOffset
					if p.seci == 0 {
						// 22:18+0530
						p.minlen = i - p.mini
					} else {
						if p.seclen == 0 {
							p.seclen = i - p.seci
						}
						if p.msi > 0 && p.mslen == 0 {
							p.mslen = i - p.msi
						}
					}
					p.offseti = i
				case '.':
					p.stateTime = timePeriod
					p.seclen = i - p.seci
					p.msi = i + 1
				case 'Z':
					p.stateTime = timeZ
					if p.seci == 0 {
						p.minlen = i - p.mini
					} else {
						p.seclen = i - p.seci
					}
					// (Z)ulu time
					p.loc = time.UTC
				case 'a', 'A':
					if p.nextIs(i, 't') || p.nextIs(i, 'T') {
						//                    x
						// September 17, 2012 at 5:00pm UTC-05
						i++ // skip t
						if p.nextIs(i, ' ') {
							//                      x
							// September 17, 2012 at 5:00pm UTC-05
							i++         // skip '
							p.houri = 0 // reset hour
						}
					} else {
						switch {
						case r == 'a' && p.nextIs(i, 'm'):
							p.coalesceTime(i)
							p.set(i, "am")
						case r == 'A' && p.nextIs(i, 'M'):
							p.coalesceTime(i)
							p.set(i, "PM")
						}
					}

				case 'p', 'P':
					// Could be AM/PM
					switch {
					case r == 'p' && p.nextIs(i, 'm'):
						p.coalesceTime(i)
						p.set(i, "pm")
					case r == 'P' && p.nextIs(i, 'M'):
						p.coalesceTime(i)
						p.set(i, "PM")
					}
				case ' ':
					p.coalesceTime(i)
					p.stateTime = timeWs
				case ':':
					if p.mini == 0 {
						p.mini = i + 1
						p.hourlen = i - p.houri
					} else if p.seci == 0 {
						p.seci = i + 1
						p.minlen = i - p.mini
					} else if p.seci > 0 {
						// 18:31:59:257    ms uses colon, wtf
						p.seclen = i - p.seci
						p.set(p.seci, "05")
						p.msi = i + 1

						// gross, gross, gross.   manipulating the datestr is horrible.
						// https://github.com/araddon/dateparse/issues/117
						// Could not get the parsing to work using golang time.Parse() without
						// replacing that colon with period.
						p.set(i, ".")
						datestr = datestr[0:i] + "." + datestr[i+1:]
						p.datestr = datestr
					}
				}
			case timeOffset:
				// 19:55:00+0100
				// timeOffsetColon
				//   15:04:05+07:00
				//   15:04:05-07:00
				if r == ':' {
					p.stateTime = timeOffsetColon
				}
			case timeWs:
				// timeWsAlpha
				//   06:20:00 UTC
				//   06:20:00 UTC-05
				//   15:44:11 UTC+0100 2015
				//   18:04:07 GMT+0100 (GMT Daylight Time)
				//   17:57:51 MST 2009
				//   timeWsAMPMMaybe
				//     05:24:37 PM
				// timeWsOffset
				//   15:04:05 -0700
				//   00:12:00 +0000 UTC
				//   timeWsOffsetColon
				//     15:04:05 -07:00
				//     17:57:51 -0700 2009
				//     timeWsOffsetColonAlpha
				//       00:12:00 +00:00 UTC
				// timeWsYear
				//     00:12:00 2008
				// timeZ
				//   15:04:05.99Z
				switch r {
				case 'A', 'P':
					// Could be AM/PM or could be PST or similar
					p.tzi = i
					p.stateTime = timeWsAMPMMaybe
				case '+', '-':
					p.offseti = i
					p.stateTime = timeWsOffset
				default:
					if unicode.IsLetter(r) {
						// 06:20:00 UTC
						// 06:20:00 UTC-05
						// 15:44:11 UTC+0100 2015
						// 17:57:51 MST 2009
						p.tzi = i
						p.stateTime = timeWsAlpha
					} else if unicode.IsDigit(r) {
						// 00:12:00 2008
						p.stateTime = timeWsYear
						p.yeari = i
					}
				}
			case timeWsAlpha:
				// 06:20:00 UTC
				// 06:20:00 UTC-05
				// timeWsAlphaWs
				//   17:57:51 MST 2009
				// timeWsAlphaZoneOffset
				// timeWsAlphaZoneOffsetWs
				//   timeWsAlphaZoneOffsetWsExtra
				//     18:04:07 GMT+0100 (GMT Daylight Time)
				//   timeWsAlphaZoneOffsetWsYear
				//     15:44:11 UTC+0100 2015
				switch r {
				case '+', '-':
					p.tzlen = i - p.tzi
					if p.tzlen == 4 {
						p.set(p.tzi, " MST")
					} else if p.tzlen == 3 {
						p.set(p.tzi, "MST")
					}
					p.stateTime = timeWsAlphaZoneOffset
					p.offseti = i
				case ' ':
					// 17:57:51 MST 2009
					// 17:57:51 MST
					p.tzlen = i - p.tzi
					if p.tzlen == 4 {
						p.set(p.tzi, " MST")
					} else if p.tzlen == 3 {
						p.set(p.tzi, "MST")
					}
					p.stateTime = timeWsAlphaWs
					p.yeari = i + 1
				}
			case timeWsAlphaWs:
				//   17:57:51 MST 2009

			case timeWsAlphaZoneOffset:
				// 06:20:00 UTC-05
				// timeWsAlphaZoneOffset
				// timeWsAlphaZoneOffsetWs
				//   timeWsAlphaZoneOffsetWsExtra
				//     18:04:07 GMT+0100 (GMT Daylight Time)
				//   timeWsAlphaZoneOffsetWsYear
				//     15:44:11 UTC+0100 2015
				switch r {
				case ' ':
					p.set(p.offseti, "-0700")
					if p.yeari == 0 {
						p.yeari = i + 1
					}
					p.stateTime = timeWsAlphaZoneOffsetWs
				}
			case timeWsAlphaZoneOffsetWs:
				// timeWsAlphaZoneOffsetWs
				//   timeWsAlphaZoneOffsetWsExtra
				//     18:04:07 GMT+0100 (GMT Daylight Time)
				//   timeWsAlphaZoneOffsetWsYear
				//     15:44:11 UTC+0100 2015
				if unicode.IsDigit(r) {
					p.stateTime = timeWsAlphaZoneOffsetWsYear
				} else {
					p.extra = i - 1
					p.stateTime = timeWsAlphaZoneOffsetWsExtra
				}
			case timeWsAlphaZoneOffsetWsYear:
				// 15:44:11 UTC+0100 2015
				if unicode.IsDigit(r) {
					p.yearlen = i - p.yeari + 1
					if p.yearlen == 4 {
						p.setYear()
					}
				}
			case timeWsAMPMMaybe:
				// timeWsAMPMMaybe
				//   timeWsAMPM
				//     05:24:37 PM
				//   timeWsAlpha
				//     00:12:00 PST
				//     15:44:11 UTC+0100 2015
				if r == 'M' {
					//return parse("2006-01-02 03:04:05 PM", datestr, loc)
					p.stateTime = timeWsAMPM
					p.set(i-1, "PM")
					if p.hourlen == 2 {
						p.set(p.houri, "03")
					} else if p.hourlen == 1 {
						p.set(p.houri, "3")
					}
				} else {
					p.stateTime = timeWsAlpha
				}

			case timeWsOffset:
				// timeWsOffset
				//   15:04:05 -0700
				//   timeWsOffsetWsOffset
				//     17:57:51 -0700 -07
				//   timeWsOffsetWs
				//     17:57:51 -0700 2009
				//     00:12:00 +0000 UTC
				//   timeWsOffsetColon
				//     15:04:05 -07:00
				//     timeWsOffsetColonAlpha
				//       00:12:00 +00:00 UTC
				switch r {
				case ':':
					p.stateTime = timeWsOffsetColon
				case ' ':
					p.set(p.offseti, "-0700")
					p.yeari = i + 1
					p.stateTime = timeWsOffsetWs
				}
			case timeWsOffsetWs:
				// 17:57:51 -0700 2009
				// 00:12:00 +0000 UTC
				// 22:18:00.001 +0000 UTC m=+0.000000001
				// w Extra
				//   17:57:51 -0700 -07
				switch r {
				case '=':
					// eff you golang
					if datestr[i-1] == 'm' {
						p.extra = i - 2
						p.trimExtra()
						break
					}
				case '+', '-', '(':
					// This really doesn't seem valid, but for some reason when round-tripping a go date
					// their is an extra +03 printed out.  seems like go bug to me, but, parsing anyway.
					// 00:00:00 +0300 +03
					// 00:00:00 +0300 +0300
					p.extra = i - 1
					p.stateTime = timeWsOffset
					p.trimExtra()
					break
				default:
					switch {
					case unicode.IsDigit(r):
						p.yearlen = i - p.yeari + 1
						if p.yearlen == 4 {
							p.setYear()
						}
					case unicode.IsLetter(r):
						// 15:04:05 -0700 MST
						if p.tzi == 0 {
							p.tzi = i
						}
					}
				}

			case timeWsOffsetColon:
				// timeWsOffsetColon
				//   15:04:05 -07:00
				//   timeWsOffsetColonAlpha
				//     2015-02-18 00:12:00 +00:00 UTC
				if unicode.IsLetter(r) {
					// 2015-02-18 00:12:00 +00:00 UTC
					p.stateTime = timeWsOffsetColonAlpha
					break iterTimeRunes
				}
			case timePeriod:
				// 15:04:05.999999999+07:00
				// 15:04:05.999999999-07:00
				// 15:04:05.999999+07:00
				// 15:04:05.999999-07:00
				// 15:04:05.999+07:00
				// 15:04:05.999-07:00
				// timePeriod
				//   17:24:37.3186369
				//   00:07:31.945167
				//   18:31:59.257000000
				//   00:00:00.000
				//   timePeriodOffset
				//     19:55:00.799+0100
				//     timePeriodOffsetColon
				//       15:04:05.999-07:00
				//   timePeriodWs
				//     timePeriodWsOffset
				//       00:07:31.945167 +0000
				//       00:00:00.000 +0000
				//       With Extra
				//         00:00:00.000 +0300 +03
				//     timePeriodWsOffsetAlpha
				//       00:07:31.945167 +0000 UTC
				//       00:00:00.000 +0000 UTC
				//       22:18:00.001 +0000 UTC m=+0.000000001
				//     timePeriodWsAlpha
				//       06:20:00.000 UTC
				switch r {
				case ' ':
					p.mslen = i - p.msi
					p.stateTime = timePeriodWs
				case '+', '-':
					// This really shouldn't happen
					p.mslen = i - p.msi
					p.offseti = i
					p.stateTime = timePeriodOffset
				default:
					if unicode.IsLetter(r) {
						// 06:20:00.000 UTC
						p.mslen = i - p.msi
						p.stateTime = timePeriodWsAlpha
					}
				}
			case timePeriodOffset:
				// timePeriodOffset
				//   19:55:00.799+0100
				//   timePeriodOffsetColon
				//     15:04:05.999-07:00
				//     13:31:51.999-07:00 MST
				if r == ':' {
					p.stateTime = timePeriodOffsetColon
				}
			case timePeriodOffsetColon:
				// timePeriodOffset
				//   timePeriodOffsetColon
				//     15:04:05.999-07:00
				//     13:31:51.999 -07:00 MST
				switch r {
				case ' ':
					p.set(p.offseti, "-07:00")
					p.stateTime = timePeriodOffsetColonWs
					p.tzi = i + 1
				}
			case timePeriodOffsetColonWs:
				// continue
			case timePeriodWs:
				// timePeriodWs
				//   timePeriodWsOffset
				//     00:07:31.945167 +0000
				//     00:00:00.000 +0000
				//   timePeriodWsOffsetAlpha
				//     00:07:31.945167 +0000 UTC
				//     00:00:00.000 +0000 UTC
				//   timePeriodWsOffsetColon
				//     13:31:51.999 -07:00 MST
				//   timePeriodWsAlpha
				//     06:20:00.000 UTC
				if p.offseti == 0 {
					p.offseti = i
				}
				switch r {
				case '+', '-':
					p.mslen = i - p.msi - 1
					p.stateTime = timePeriodWsOffset
				default:
					if unicode.IsLetter(r) {
						//     00:07:31.945167 +0000 UTC
						//     00:00:00.000 +0000 UTC
						p.stateTime = timePeriodWsOffsetWsAlpha
						break iterTimeRunes
					}
				}

			case timePeriodWsOffset:
				// timePeriodWs
				//   timePeriodWsOffset
				//     00:07:31.945167 +0000
				//     00:00:00.000 +0000
				//     With Extra
				//       00:00:00.000 +0300 +03
				//   timePeriodWsOffsetAlpha
				//     00:07:31.945167 +0000 UTC
				//     00:00:00.000 +0000 UTC
				//     03:02:00.001 +0300 MSK m=+0.000000001
				//   timePeriodWsOffsetColon
				//     13:31:51.999 -07:00 MST
				//   timePeriodWsAlpha
				//     06:20:00.000 UTC
				switch r {
				case ':':
					p.stateTime = timePeriodWsOffsetColon
				case ' ':
					p.set(p.offseti, "-0700")
				case '+', '-':
					// This really doesn't seem valid, but for some reason when round-tripping a go date
					// their is an extra +03 printed out.  seems like go bug to me, but, parsing anyway.
					// 00:00:00.000 +0300 +03
					// 00:00:00.000 +0300 +0300
					p.extra = i - 1
					p.trimExtra()
					break
				default:
					if unicode.IsLetter(r) {
						// 00:07:31.945167 +0000 UTC
						// 00:00:00.000 +0000 UTC
						// 03:02:00.001 +0300 MSK m=+0.000000001
						p.stateTime = timePeriodWsOffsetWsAlpha
					}
				}
			case timePeriodWsOffsetWsAlpha:
				// 03:02:00.001 +0300 MSK m=+0.000000001
				// eff you golang
				if r == '=' && datestr[i-1] == 'm' {
					p.extra = i - 2
					p.trimExtra()
					break
				}

			case timePeriodWsOffsetColon:
				// 13:31:51.999 -07:00 MST
				switch r {
				case ' ':
					p.set(p.offseti, "-07:00")
				default:
					if unicode.IsLetter(r) {
						// 13:31:51.999 -07:00 MST
						p.tzi = i
						p.stateTime = timePeriodWsOffsetColonAlpha
					}
				}
			case timePeriodWsOffsetColonAlpha:
				// continue
			case timeZ:
				// timeZ
				//   15:04:05.99Z
				// With a time-zone at end after Z
				// 2006-01-02T15:04:05.999999999Z07:00
				// 2006-01-02T15:04:05Z07:00
				// RFC3339     = "2006-01-02T15:04:05Z07:00"
				// RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
				if unicode.IsDigit(r) {
					p.stateTime = timeZDigit
				}

			}
		}

		switch p.stateTime {
		case timeWsAlpha:
			switch len(p.datestr) - p.tzi {
			case 3:
				// 13:31:51.999 +01:00 CET
				p.set(p.tzi, "MST")
			case 4:
				p.set(p.tzi, "MST")
				p.extra = len(p.datestr) - 1
				p.trimExtra()
			}

		case timeWsAlphaWs:
			p.yearlen = i - p.yeari
			p.setYear()
		case timeWsYear:
			p.yearlen = i - p.yeari
			p.setYear()
		case timeWsAlphaZoneOffsetWsExtra:
			p.trimExtra()
		case timeWsAlphaZoneOffset:
			// 06:20:00 UTC-05
			if i-p.offseti < 4 {
				p.set(p.offseti, "-07")
			} else {
				p.set(p.offseti, "-0700")
			}

		case timePeriod:
			p.mslen = i - p.msi
		case timeOffset:

			switch len(p.datestr) - p.offseti {
			case 0, 1, 2, 4:
				return p, fmt.Errorf("TZ offset not recognized %q near %q (must be 2 or 4 digits optional colon)", datestr, string(datestr[p.offseti:]))
			case 3:
				// 19:55:00+01
				p.set(p.offseti, "-07")
			case 5:
				// 19:55:00+0100
				p.set(p.offseti, "-0700")
			}

		case timeWsOffset:
			p.set(p.offseti, "-0700")
		case timeWsOffsetWs:
			// 17:57:51 -0700 2009
			// 00:12:00 +0000 UTC
			if p.tzi > 0 {
				switch len(p.datestr) - p.tzi {
				case 3:
					// 13:31:51.999 +01:00 CET
					p.set(p.tzi, "MST")
				case 4:
					// 13:31:51.999 +01:00 CEST
					p.set(p.tzi, "MST ")
				}

			}
		case timeWsOffsetColon:
			// 17:57:51 -07:00
			p.set(p.offseti, "-07:00")
		case timeOffsetColon:
			// 15:04:05+07:00
			p.set(p.offseti, "-07:00")
		case timePeriodOffset:
			// 19:55:00.799+0100
			p.set(p.offseti, "-0700")
		case timePeriodOffsetColon:
			p.set(p.offseti, "-07:00")
		case timePeriodWsOffsetColonAlpha:
			p.tzlen = i - p.tzi
			switch p.tzlen {
			case 3:
				p.set(p.tzi, "MST")
			case 4:
				p.set(p.tzi, "MST ")
			}
		case timePeriodWsOffset:
			p.set(p.offseti, "-0700")
		}
		p.coalesceTime(i)
	}

	switch p.stateDate {
	case dateDigit:
		// unixy timestamps ish
		//  example              ct type
		//  1499979655583057426  19 nanoseconds
		//  1499979795437000     16 micro-seconds
		//  20180722105203       14 yyyyMMddhhmmss
		//  1499979795437        13 milliseconds
		//  1332151919           10 seconds
		//  20140601             8  yyyymmdd
		//  2014                 4  yyyy
		t := time.Time{}
		if len(datestr) == len("1499979655583057426") { // 19
			// nano-seconds
			if nanoSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil {
				t = time.Unix(0, nanoSecs)
			}
		} else if len(datestr) == len("1499979795437000") { // 16
			// micro-seconds
			if microSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil {
				t = time.Unix(0, microSecs*1000)
			}
		} else if len(datestr) == len("yyyyMMddhhmmss") { // 14
			// yyyyMMddhhmmss
			p.format = []byte("20060102150405")
			return p, nil
		} else if len(datestr) == len("1332151919000") { // 13
			if miliSecs, err := strconv.ParseInt(datestr, 10, 64); err == nil {
				t = time.Unix(0, miliSecs*1000*1000)
			}
		} else if len(datestr) == len("1332151919") { //10
			if secs, err := strconv.ParseInt(datestr, 10, 64); err == nil {
				t = time.Unix(secs, 0)
			}
		} else if len(datestr) == len("20140601") {
			p.format = []byte("20060102")
			return p, nil
		} else if len(datestr) == len("2014") {
			p.format = []byte("2006")
			return p, nil
		} else if len(datestr) < 4 {
			return nil, fmt.Errorf("unrecognized format, too short %v", datestr)
		}
		if !t.IsZero() {
			if loc == nil {
				p.t = &t
				return p, nil
			}
			t = t.In(loc)
			p.t = &t
			return p, nil
		}
	case dateDigitSt:
		// 171113 14:14:20
		return p, nil

	case dateYearDash:
		// 2006-01
		return p, nil

	case dateYearDashDash:
		// 2006-01-02
		// 2006-1-02
		// 2006-1-2
		// 2006-01-2
		return p, nil

	case dateYearDashDashOffset:
		///  2020-07-20+00:00
		switch len(p.datestr) - p.offseti {
		case 5:
			p.set(p.offseti, "-0700")
		case 6:
			p.set(p.offseti, "-07:00")
		}
		return p, nil

	case dateYearDashAlphaDash:
		// 2013-Feb-03
		// 2013-Feb-3
		p.daylen = i - p.dayi
		p.setDay()
		return p, nil

	case dateYearDashDashWs:
		// 2013-04-01
		return p, nil

	case dateYearDashDashT:
		return p, nil

	case dateDigitDashAlphaDash:
		// 13-Feb-03   ambiguous
		// 28-Feb-03   ambiguous
		// 29-Jun-2016
		length := len(datestr) - (p.moi + p.molen + 1)
		if length == 4 {
			p.yearlen = 4
			p.set(p.yeari, "2006")
			// We now also know that part1 was the day
			p.dayi = 0
			p.daylen = p.part1Len
			p.setDay()
		} else if length == 2 {
			// We have no idea if this is
			// yy-mon-dd   OR  dd-mon-yy
			//
			// We are going to ASSUME (bad, bad) that it is dd-mon-yy  which is a horible assumption
			p.ambiguousMD = true
			p.yearlen = 2
			p.set(p.yeari, "06")
			// We now also know that part1 was the day
			p.dayi = 0
			p.daylen = p.part1Len
			p.setDay()
		}

		return p, nil

	case dateDigitDot:
		// 2014.05
		p.molen = i - p.moi
		p.setMonth()
		return p, nil

	case dateDigitDotDot:
		// 03.31.1981
		// 3.31.2014
		// 3.2.1981
		// 3.2.81
		// 08.21.71
		// 2018.09.30
		return p, nil

	case dateDigitWsMoYear:
		// 2 Jan 2018
		// 2 Jan 18
		// 2 Jan 2018 23:59
		// 02 Jan 2018 23:59
		// 12 Feb 2006, 19:17
		return p, nil

	case dateDigitWsMolong:
		// 18 January 2018
		// 8 January 2018
		if p.daylen == 2 {
			p.format = []byte("02 January 2006")
			return p, nil
		}
		p.format = []byte("2 January 2006")
		return p, nil // parse("2 January 2006", datestr, loc)

	case dateAlphaWsMonth:
		p.yearlen = i - p.yeari
		p.setYear()
		return p, nil

	case dateAlphaWsMonthMore:
		return p, nil

	case dateAlphaWsDigitMoreWs:
		// oct 1, 1970
		p.yearlen = i - p.yeari
		p.setYear()
		return p, nil

	case dateAlphaWsDigitMoreWsYear:
		// May 8, 2009 5:57:51 PM
		// Jun 7, 2005, 05:57:51
		return p, nil

	case dateAlphaWsAlpha:
		return p, nil

	case dateAlphaWsDigit:
		return p, nil

	case dateAlphaWsDigitYearmaybe:
		return p, nil

	case dateDigitSlash:
		// 3/1/2014
		// 10/13/2014
		// 01/02/2006
		return p, nil

	case dateDigitSlashAlpha:
		// 03/Jun/2014
		return p, nil

	case dateDigitYearSlash:
		// 2014/10/13
		return p, nil

	case dateDigitColon:
		// 3:1:2014
		// 10:13:2014
		// 01:02:2006
		// 2014:10:13
		return p, nil

	case dateDigitChineseYear:
		// dateDigitChineseYear
		//   2014年04月08日
		p.format = []byte("2006年01月02日")
		return p, nil

	case dateDigitChineseYearWs:
		p.format = []byte("2006年01月02日 15:04:05")
		return p, nil

	case dateWeekdayComma:
		// Monday, 02 Jan 2006 15:04:05 -0700
		// Monday, 02 Jan 2006 15:04:05 +0100
		// Monday, 02-Jan-06 15:04:05 MST
		return p, nil

	case dateWeekdayAbbrevComma:
		// Mon, 02-Jan-06 15:04:05 MST
		// Mon, 02 Jan 2006 15:04:05 MST
		return p, nil

	}

	return nil, unknownErr(datestr)
}

type parser struct {
	loc                        *time.Location
	preferMonthFirst           bool
	retryAmbiguousDateWithSwap bool
	ambiguousMD                bool
	stateDate                  dateState
	stateTime                  timeState
	format                     []byte
	datestr                    string
	fullMonth                  string
	skip                       int
	extra                      int
	part1Len                   int
	yeari                      int
	yearlen                    int
	moi                        int
	molen                      int
	dayi                       int
	daylen                     int
	houri                      int
	hourlen                    int
	mini                       int
	minlen                     int
	seci                       int
	seclen                     int
	msi                        int
	mslen                      int
	offseti                    int
	offsetlen                  int
	tzi                        int
	tzlen                      int
	t                          *time.Time
}

// ParserOption defines a function signature implemented by options
// Options defined like this accept the parser and operate on the data within
type ParserOption func(*parser) error

// PreferMonthFirst is an option that allows preferMonthFirst to be changed from its default
func PreferMonthFirst(preferMonthFirst bool) ParserOption {
	return func(p *parser) error {
		p.preferMonthFirst = preferMonthFirst
		return nil
	}
}

// RetryAmbiguousDateWithSwap is an option that allows retryAmbiguousDateWithSwap to be changed from its default
func RetryAmbiguousDateWithSwap(retryAmbiguousDateWithSwap bool) ParserOption {
	return func(p *parser) error {
		p.retryAmbiguousDateWithSwap = retryAmbiguousDateWithSwap
		return nil
	}
}

func newParser(dateStr string, loc *time.Location, opts ...ParserOption) *parser {
	p := &parser{
		stateDate:                  dateStart,
		stateTime:                  timeIgnore,
		datestr:                    dateStr,
		loc:                        loc,
		preferMonthFirst:           true,
		retryAmbiguousDateWithSwap: false,
	}
	p.format = []byte(dateStr)

	// allow the options to mutate the parser fields from their defaults
	for _, option := range opts {
		option(p)
	}
	return p
}

func (p *parser) nextIs(i int, b byte) bool {
	if len(p.datestr) > i+1 && p.datestr[i+1] == b {
		return true
	}
	return false
}

func (p *parser) set(start int, val string) {
	if start < 0 {
		return
	}
	if len(p.format) < start+len(val) {
		return
	}
	for i, r := range val {
		p.format[start+i] = byte(r)
	}
}
func (p *parser) setMonth() {
	if p.molen == 2 {
		p.set(p.moi, "01")
	} else if p.molen == 1 {
		p.set(p.moi, "1")
	}
}

func (p *parser) setDay() {
	if p.daylen == 2 {
		p.set(p.dayi, "02")
	} else if p.daylen == 1 {
		p.set(p.dayi, "2")
	}
}
func (p *parser) setYear() {
	if p.yearlen == 2 {
		p.set(p.yeari, "06")
	} else if p.yearlen == 4 {
		p.set(p.yeari, "2006")
	}
}
func (p *parser) coalesceDate(end int) {
	if p.yeari > 0 {
		if p.yearlen == 0 {
			p.yearlen = end - p.yeari
		}
		p.setYear()
	}
	if p.moi > 0 && p.molen == 0 {
		p.molen = end - p.moi
		p.setMonth()
	}
	if p.dayi > 0 && p.daylen == 0 {
		p.daylen = end - p.dayi
		p.setDay()
	}
}
func (p *parser) ts() string {
	return fmt.Sprintf("h:(%d:%d) m:(%d:%d) s:(%d:%d)", p.houri, p.hourlen, p.mini, p.minlen, p.seci, p.seclen)
}
func (p *parser) ds() string {
	return fmt.Sprintf("%s d:(%d:%d) m:(%d:%d) y:(%d:%d)", p.datestr, p.dayi, p.daylen, p.moi, p.molen, p.yeari, p.yearlen)
}
func (p *parser) coalesceTime(end int) {
	// 03:04:05
	// 15:04:05
	// 3:04:05
	// 3:4:5
	// 15:04:05.00
	if p.houri > 0 {
		if p.hourlen == 2 {
			p.set(p.houri, "15")
		} else if p.hourlen == 1 {
			p.set(p.houri, "3")
		}
	}
	if p.mini > 0 {
		if p.minlen == 0 {
			p.minlen = end - p.mini
		}
		if p.minlen == 2 {
			p.set(p.mini, "04")
		} else {
			p.set(p.mini, "4")
		}
	}
	if p.seci > 0 {
		if p.seclen == 0 {
			p.seclen = end - p.seci
		}
		if p.seclen == 2 {
			p.set(p.seci, "05")
		} else {
			p.set(p.seci, "5")
		}
	}

	if p.msi > 0 {
		for i := 0; i < p.mslen; i++ {
			p.format[p.msi+i] = '0'
		}
	}
}
func (p *parser) setFullMonth(month string) {
	if p.moi == 0 {
		p.format = []byte(fmt.Sprintf("%s%s", "January", p.format[len(month):]))
	}
}

func (p *parser) trimExtra() {
	if p.extra > 0 && len(p.format) > p.extra {
		p.format = p.format[0:p.extra]
		p.datestr = p.datestr[0:p.extra]
	}
}

// func (p *parser) remove(i, length int) {
// 	if len(p.format) > i+length {
// 		//append(a[:i], a[j:]...)
// 		p.format = append(p.format[0:i], p.format[i+length:]...)
// 	}
// 	if len(p.datestr) > i+length {
// 		//append(a[:i], a[j:]...)
// 		p.datestr = fmt.Sprintf("%s%s", p.datestr[0:i], p.datestr[i+length:])
// 	}
// }

func (p *parser) parse() (time.Time, error) {
	if p.t != nil {
		return *p.t, nil
	}
	if len(p.fullMonth) > 0 {
		p.setFullMonth(p.fullMonth)
	}
	if p.skip > 0 && len(p.format) > p.skip {
		p.format = p.format[p.skip:]
		p.datestr = p.datestr[p.skip:]
	}

	if p.loc == nil {
		// gou.Debugf("parse layout=%q input=%q   \ntx, err := time.Parse(%q, %q)", string(p.format), p.datestr, string(p.format), p.datestr)
		return time.Parse(string(p.format), p.datestr)
	}
	//gou.Debugf("parse layout=%q input=%q   \ntx, err := time.ParseInLocation(%q, %q, %v)", string(p.format), p.datestr, string(p.format), p.datestr, p.loc)
	return time.ParseInLocation(string(p.format), p.datestr, p.loc)
}
func isDay(alpha string) bool {
	for _, day := range days {
		if alpha == day {
			return true
		}
	}
	return false
}
func isMonthFull(alpha string) bool {
	for _, month := range months {
		if alpha == month {
			return true
		}
	}
	return false
}