//
//  tank
//
//  Import and Playlist Daemon for autoradio project
//
//
//  Copyright (C) 2017-2018 Christian Pointner <equinox@helsinki.at>
//
//  This file is part of tank.
//
//  tank is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  any later version.
//
//  tank is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with tank. If not, see <http://www.gnu.org/licenses/>.
//

package store

import (
	"encoding/json"
	"errors"

	"github.com/coreos/bbolt"
)

func getFilesAndImportsBucket(tx *bolt.Tx, group string) (fb, ib *bolt.Bucket, err error) {
	var gb *bolt.Bucket
	if gb, err = getGroupBucket(tx, group); err != nil {
		return
	}
	if gb == nil {
		if !tx.Writable() {
			return // for read-only transactions this function might return files and imports = nil
		}
		if gb, err = createGroup(tx, group); err != nil {
			return
		}
	}

	fb = gb.Bucket([]byte(filesBn))
	if fb == nil {
		err = errors.New("invalid store: files bucket of group '" + group + "' not found")
		return
	}

	ib = gb.Bucket([]byte(importsBn))
	if ib == nil {
		err = errors.New("invalid store: imports bucket of group '" + group + "' not found")
		return
	}

	return
}

func markImportAborted(tx *bolt.Tx, fb *bolt.Bucket, k []byte) (err error) {
	bFile := fb.Get(k)
	if bFile == nil {
		return nil // this is not really a poblem, the corresponding import will be deleted anyway
	}
	var file File
	if err = json.Unmarshal(bFile, &file); err != nil {
		return
	}
	file.Source.Import.State = ImportAborted
	file.Source.Import.Success = false
	file.Source.Import.Log.Append("aborted after unexpected error")
	return
}

func cleanupAndRepairFilesAndImportsOfGroup(tx *bolt.Tx, gb *bolt.Bucket) (err error) {
	var fb, ib *bolt.Bucket
	fb, err = gb.CreateBucketIfNotExists([]byte(filesBn))
	if err != nil {
		return
	}
	ib, err = gb.CreateBucketIfNotExists([]byte(importsBn))
	if err != nil {
		return
	}

	ic := ib.Cursor()
	for k, v := ic.First(); k != nil; k, v = ic.Next() {
		if v == nil {
			// TODO: delete bucket keys?
			continue
		}

		is := ImportNew
		is.UnmarshalText(v) // ignoring errors here and treat the case as if state == New (aka delete the corresponding file object)"
		if is == ImportNew {
			fb.Delete(k)
		} else {
			if err = markImportAborted(tx, fb, k); err != nil {
				return
			}
		}
		ib.Delete(k)
	}
	return
}

func (st *Store) ListFiles(group string) (files Files, err error) {
	err = st.db.View(func(tx *bolt.Tx) error {
		fb, _, err := getFilesAndImportsBucket(tx, group)
		if err != nil {
			return err
		}
		if fb == nil { // this is a read-only transaction getFilesBucket might return fb = nil
			return nil
		}
		files = make(Files)

		c := fb.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			var file File
			if err := json.Unmarshal(v, &file); err != nil {
				return err
			}
			files[btoi(k)] = file
		}
		return err
	})
	return
}

func (st *Store) CreateFile(group string, file File) (id uint64, err error) {
	err = st.db.Update(func(tx *bolt.Tx) error {
		fb, ib, err := getFilesAndImportsBucket(tx, group)
		if err != nil {
			return err
		}
		if id, err = getNextID(tx); err != nil {
			return err
		}
		bFile, err := json.Marshal(file)
		if err != nil {
			return err
		}
		bImportState, err := ImportNew.MarshalText()
		if err != nil {
			return err
		}
		if err = ib.Put(itob(id), bImportState); err != nil {
			return err
		}
		return fb.Put(itob(id), bFile)
	})
	return
}

func (st *Store) GetFile(group string, id uint64) (file File, err error) {
	err = st.db.View(func(tx *bolt.Tx) error {
		fb, _, err := getFilesAndImportsBucket(tx, group)
		if err != nil {
			return err
		}
		if fb == nil { // this is a read-only transaction getFilesBucket might return fb = nil
			return ErrNotFound
		}
		bFile := fb.Get(itob(id))
		if bFile == nil {
			return ErrNotFound
		}
		return json.Unmarshal(bFile, &file)
	})
	return
}

func (st *Store) UpdateFile(group string, id uint64, file File) (err error) {
	err = st.db.Update(func(tx *bolt.Tx) error {
		fb, _, err := getFilesAndImportsBucket(tx, group)
		if err != nil {
			return err
		}
		bFile := fb.Get(itob(id))
		if bFile == nil {
			return ErrNotFound
		}
		bFile, err = json.Marshal(file)
		if err != nil {
			return err
		}
		// TODO: update metdata fileheader
		// TODO: update affiliation with auto-playlists
		return fb.Put(itob(id), bFile)
	})
	return
}

func (st *Store) DeleteFile(group string, id uint64) (err error) {
	err = st.db.Update(func(tx *bolt.Tx) error {
		fb, _, err := getFilesAndImportsBucket(tx, group)
		if err != nil {
			return err
		}
		bFile := fb.Get(itob(id))
		if bFile == nil {
			return ErrNotFound
		}
		var file File
		if err = json.Unmarshal(bFile, &file); err != nil {
			return err
		}
		if len(file.UsedBy) > 0 {
			return ErrFileInUse
		}
		// TODO: remove file from auto-playlists
		// TODO: delete file in filesystem
		return fb.Delete(itob(id))
	})
	return
}