// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0

package spec

import (
	"flag"
	"fmt"
	"net/url"
	"regexp"
	"strings"

	"github.com/attic-labs/noms/go/chunks"
	"github.com/attic-labs/noms/go/d"
	"github.com/attic-labs/noms/go/datas"
	"github.com/attic-labs/noms/go/dataset"
	"github.com/attic-labs/noms/go/types"
)

var (
	datasetRe = regexp.MustCompile("^" + dataset.DatasetRe.String() + "$")
	ldbStores = map[string]*refCountingLdbStore{}
)

func GetDatabase(str string) (datas.Database, error) {
	sp, err := parseDatabaseSpec(str)
	if err != nil {
		return nil, err
	}
	return sp.Database()
}

func GetChunkStore(str string) (chunks.ChunkStore, error) {
	sp, err := parseDatabaseSpec(str)
	if err != nil {
		return nil, err
	}

	switch sp.Protocol {
	case "ldb":
		return getLDBStore(sp.Path), nil
	case "mem":
		return chunks.NewMemoryStore(), nil
	default:
		return nil, fmt.Errorf("Unable to create chunkstore for protocol: %s", str)
	}
}

func GetDataset(str string) (dataset.Dataset, error) {
	sp, err := parseDatasetSpec(str)
	if err != nil {
		return dataset.Dataset{}, err
	}
	return sp.Dataset()
}

func GetPath(str string) (datas.Database, types.Value, error) {
	sp, err := parsePathSpec(str)
	if err != nil {
		return nil, nil, err
	}
	return sp.Value()
}

type databaseSpec struct {
	Protocol    string
	Path        string
	accessToken string
}

type datasetSpec struct {
	DbSpec      databaseSpec
	DatasetName string
}

type pathSpec struct {
	DbSpec databaseSpec
	Path   AbsolutePath
}

func parseDatabaseSpec(spec string) (databaseSpec, error) {
	ldbDatabaseSpec := func(path string) (databaseSpec, error) {
		if len(path) == 0 {
			return databaseSpec{}, fmt.Errorf("Empty file system path")
		}
		return databaseSpec{Protocol: "ldb", Path: path}, nil
	}

	parts := strings.SplitN(spec, ":", 2) // [protocol] [, path]?
	protocol := parts[0]

	// If there was no ":" then this is either a mem spec, or a filesystem path.
	// This is ambiguous if the file system path is "mem" but that just means the path needs to be explicitly "ldb:mem".
	if len(parts) == 1 {
		if protocol == "mem" {
			return databaseSpec{Protocol: "mem"}, nil
		}
		return ldbDatabaseSpec(protocol)
	}

	path := parts[1]

	switch protocol {
	case "http", "https":
		u, err := url.Parse(spec)
		if err != nil || len(u.Host) == 0 {
			return databaseSpec{}, fmt.Errorf("Invalid URL: %s", spec)
		}
		token := u.Query().Get("access_token")
		return databaseSpec{Protocol: protocol, Path: path, accessToken: token}, nil

	case "ldb":
		return ldbDatabaseSpec(path)

	case "mem":
		return databaseSpec{}, fmt.Errorf(`In-memory database must be specified as "mem", not "mem:%s"`, path)

	default:
		return databaseSpec{}, fmt.Errorf("Invalid database protocol: %s", spec)
	}
}

func splitAndParseDatabaseSpec(spec string) (databaseSpec, string, error) {
	parts := strings.SplitN(spec, "::", 2)
	if len(parts) != 2 {
		return databaseSpec{}, "", fmt.Errorf("Missing :: separator between database and dataset: %s", spec)
	}

	dbSpec, err := parseDatabaseSpec(parts[0])
	if err != nil {
		return databaseSpec{}, "", err
	}

	return dbSpec, parts[1], nil
}

func parseDatasetSpec(spec string) (datasetSpec, error) {
	dbSpec, dsName, err := splitAndParseDatabaseSpec(spec)
	if err != nil {
		return datasetSpec{}, err
	}

	if !datasetRe.MatchString(dsName) {
		return datasetSpec{}, fmt.Errorf("Invalid dataset, must match %s: %s", dataset.DatasetRe.String(), dsName)
	}

	return datasetSpec{dbSpec, dsName}, nil
}

func parsePathSpec(spec string) (pathSpec, error) {
	dbSpec, pathStr, err := splitAndParseDatabaseSpec(spec)
	if err != nil {
		return pathSpec{}, err
	}

	path, err := NewAbsolutePath(pathStr)
	if err != nil {
		return pathSpec{}, err
	}

	return pathSpec{dbSpec, path}, nil
}

func (s databaseSpec) String() string {
	return s.Protocol + ":" + s.Path
}

func (spec databaseSpec) Database() (ds datas.Database, err error) {
	switch spec.Protocol {
	case "http", "https":
		err = d.Unwrap(d.Try(func() {
			ds = datas.NewRemoteDatabase(spec.String(), "Bearer "+spec.accessToken)
		}))
	case "ldb":
		err = d.Unwrap(d.Try(func() {
			ds = datas.NewDatabase(getLDBStore(spec.Path))
		}))
	case "mem":
		ds = datas.NewDatabase(chunks.NewMemoryStore())
	default:
		err = fmt.Errorf("Invalid path prototocol: %s", spec.Protocol)
	}
	return
}

func (spec datasetSpec) Dataset() (dataset.Dataset, error) {
	store, err := spec.DbSpec.Database()
	if err != nil {
		return dataset.Dataset{}, err
	}

	return dataset.NewDataset(store, spec.DatasetName), nil
}

func (s datasetSpec) String() string {
	return s.DbSpec.String() + "::" + s.DatasetName
}

func (spec datasetSpec) Value() (datas.Database, types.Value, error) {
	dataset, err := spec.Dataset()
	if err != nil {
		return nil, nil, err
	}

	commit, ok := dataset.MaybeHead()
	if !ok {
		dataset.Database().Close()
		return nil, nil, fmt.Errorf("No head value for dataset: %s", spec.DatasetName)
	}

	return dataset.Database(), commit, nil
}

func (spec pathSpec) Value() (db datas.Database, val types.Value, err error) {
	db, err = spec.DbSpec.Database()
	if err != nil {
		return
	}

	val = spec.Path.Resolve(db)
	return
}

func RegisterDatabaseFlags(flags *flag.FlagSet) {
	chunks.RegisterLevelDBFlags(flags)
}

func CreateDatabaseSpecString(protocol, path string) string {
	return fmt.Sprintf("%s:%s", protocol, path)
}

func CreateValueSpecString(protocol, path, value string) string {
	return fmt.Sprintf("%s:%s::%s", protocol, path, value)
}

func getLDBStore(path string) chunks.ChunkStore {
	if store, ok := ldbStores[path]; ok {
		store.AddRef()
		return store
	}

	store := newRefCountingLdbStore(path, func() {
		delete(ldbStores, path)
	})
	ldbStores[path] = store
	return store
}
