package parsers

import (
	"errors"
	"fmt"
	"git.aiterp.net/rpdata/api/models"
	"strconv"
	"strings"
	"time"
)

type ParseError struct {
	Line    string
	Problem string
}

func (e *ParseError) Error() string {
	return fmt.Sprintf("Unrecognized post: %s (error: %s)", e.Line, e.Problem)
}

func IsParseError(err error) bool {
	_, ok := err.(*ParseError)
	return ok
}

// ErrEmptyLog is returned by ParseLog if there are no (valid) posts in the log.
var ErrEmptyLog = errors.New("no valid posts found in log")

// MircLog parses the log and returns the things that can be gleamed from them.
func MircLog(data string, date time.Time, strict bool) (*ParsedLog, error) {
	lines := strings.Split(data, "\n")
	posts := make([]*models.Post, 0, len(lines))
	prev := models.Post{}

	for _, line := range lines {
		line = strings.Trim(line, "\r\t  ")
		if len(line) < 1 {
			continue
		}

		post, err := MircPost(line, date, prev)
		if err != nil {
			if strict {
				return nil, err
			}

			continue
		}

		posts = append(posts, &post)
		prev = post
	}

	if len(posts) == 0 {
		return nil, ErrEmptyLog
	}

	log := models.Log{
		Date: posts[0].Time,
	}

	return &ParsedLog{log, posts}, nil
}

// MircPost parses a post from a mirc-like line. If the previous post is included (it can be empty), it will be used
// to determine whether midnight has passed.
func MircPost(line string, date time.Time, prev models.Post) (models.Post, error) {
	// Do basic validation
	line = strings.Trim(line, "  \t\n\r")
	if len(line) == 0 || !strings.HasPrefix(line, "[") {
		return models.Post{}, &ParseError{
			Line:    line,
			Problem: "no timestamp",
		}
	}

	// Parse timestamp
	tsEndIndex := strings.IndexByte(line, ']')
	if tsEndIndex == -1 || len(line) < tsEndIndex+5 {
		return models.Post{}, &ParseError{
			Line:    line,
			Problem: "incomplete timestamp",
		}
	}
	tsStr := line[1:tsEndIndex]
	tsSplit := strings.Split(tsStr, ":")
	tsUnits := make([]int, len(tsSplit))
	if len(tsSplit) < 2 {
		return models.Post{}, &ParseError{
			Line:    line,
			Problem: "invalid timestamp",
		}
	}
	for i := range tsSplit {
		n, err := strconv.Atoi(tsSplit[i])
		if err != nil {
			return models.Post{}, &ParseError{
				Line:    line,
				Problem: "invalid number in timestamp",
			}
		}

		tsUnits[i] = n
	}
	if len(tsUnits) == 2 {
		tsUnits = append(tsUnits, 0)
	}

	// Determine timestamp from parsed data and previous post.
	if !prev.Time.IsZero() {
		date = prev.Time
	}
	ts := time.Date(date.Year(), date.Month(), date.Day(), tsUnits[0], tsUnits[1], tsUnits[2], 0, date.Location())
	if ts.Before(prev.Time) {
		ts = ts.Add(time.Hour * 24)
	}

	if line[tsEndIndex+2] == '*' {
		split := strings.SplitN(line[tsEndIndex+4:], " ", 2)
		if len(split) == 1 {
			return models.Post{}, &ParseError{
				Line:    line,
				Problem: "post is empty",
			}
		}

		post := models.Post{
			ID:       "UNASSIGNED",
			LogID:    "UNASSIGNED",
			Time:     ts,
			Kind:     "action",
			Nick:     strings.Trim(strings.TrimLeft(split[0], "+@!~\u001F"), "\u001F"),
			Text:     split[1],
			Position: prev.Position + 1,
		}

		if post.Nick[0] == '=' {
			post.Kind = "scene"
		}

		return post, nil
	} else if line[tsEndIndex+2] == '<' {
		split := strings.SplitN(line[tsEndIndex+2:], " ", 2)
		if len(split) == 1 {
			return models.Post{}, &ParseError{
				Line:    line,
				Problem: "post is empty",
			}
		}

		post := models.Post{
			ID:       "UNASSIGNED",
			LogID:    "UNASSIGNED",
			Time:     ts,
			Kind:     "text",
			Nick:     strings.Trim(strings.TrimLeft(split[0][1:len(split[0])-1], "+@!~"), "\u001F"),
			Text:     split[1],
			Position: prev.Position + 1,
		}

		if post.Nick[0] == '=' {
			post.Kind = "scene"
		}

		return post, nil
	} else {
		return models.Post{}, &ParseError{
			Line:    line,
			Problem: "line is neither action nor text post",
		}
	}
}