//
//  tank
//
//  Import and Playlist Daemon for autoradio project
//
//
//  Copyright (C) 2017-2019 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 (
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"github.com/jinzhu/gorm"
)

func (st *Store) getShowPath(show string) string {
	return filepath.Join(st.basePath, show)
}

func (st *Store) createShow(tx *gorm.DB, name string) (show *Show, err error) {
	showDirPath := st.getShowPath(name)
	if err := os.Mkdir(showDirPath, 0755); err != nil {
		if !os.IsExist(err) {
			return nil, errors.New("unable to create directory for show '" + name + "': " + err.Error())
		}
		if stat, err := os.Stat(showDirPath); err != nil {
			return nil, errors.New("unable to create directory for show '" + name + "': " + err.Error())
		} else if !stat.IsDir() {
			return nil, errors.New("unable to create directory for show '" + name + "': path exists and is not a directory")
		}
	}
	show = &Show{Name: name}
	err = tx.FirstOrCreate(show).Error
	if err != nil {
		os.RemoveAll(showDirPath)
	}
	return
}

// this will not fail if the show already exists
func (st *Store) CreateShow(name string) (show *Show, err error) {
	return st.createShow(st.db, name)
}

func (st *Store) cloneFiles(tx *gorm.DB, name, from string) (fileIDMapping map[uint64]uint64, err error) {
	fileIDMapping = make(map[uint64]uint64)
	var files Files
	if err = tx.Where("show_name = ?", from).Find(&files).Error; err != nil {
		return
	}
	for _, file := range files {
		fromFileID := file.ID

		file.ID = 0
		file.ShowName = name
		if err = tx.Create(&file).Error; err != nil {
			return
		}
		if err = os.Link(st.GetFilePath(from, fromFileID), st.GetFilePath(name, file.ID)); err != nil {
			return
		}
		fileIDMapping[fromFileID] = file.ID

		var logs []ImportLog
		if err = tx.Where("file_id = ?", file.ID).Find(&logs).Error; err != nil {
			return
		}
		for _, log := range logs {
			log.ID = 0
			log.File = File{ID: file.ID, ShowName: name}
			log.FileID = 0
			if err = tx.Create(&log).Error; err != nil {
				return
			}
		}
	}
	return
}

func (st *Store) clonePlaylists(tx *gorm.DB, name, from string, fileIDMapping map[uint64]uint64) (err error) {
	var playlists Playlists
	err = tx.Where("show_name = ?", from).Preload("Entries", func(db *gorm.DB) *gorm.DB {
		return db.Order("playlist_entries.line_num asc")
	}).Preload("Entries.File").Find(&playlists).Error
	if err != nil {
		return
	}
	for _, playlist := range playlists {
		playlist.ID = 0
		playlist.ShowName = name
		for idx := range playlist.Entries {
			playlist.Entries[idx].ID = 0
			playlist.Entries[idx].PlaylistID = 0
			if playlist.Entries[idx].File != nil {
				toFileID, exists := fileIDMapping[playlist.Entries[idx].File.ID]
				if exists {
					playlist.Entries[idx].File = &File{ID: toFileID, ShowName: name}
				}
			}
		}
		if err = tx.Create(&playlist).Error; err != nil {
			return
		}
	}
	return
}

func (st *Store) CloneShow(name, from string) (show *Show, err error) {
	tx := st.db.Begin()
	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
			os.RemoveAll(st.getShowPath(name))
			if err == nil {
				err = fmt.Errorf("runtime panic: %+v", r)
			}
		}
	}()
	if err = tx.Error; err != nil {
		return nil, err
	}

	cnt := 0
	if err = tx.Model(&Show{Name: from}).Count(&cnt).Error; err != nil {
		tx.Rollback()
		return nil, err
	}
	if cnt == 0 {
		tx.Rollback()
		return nil, ErrNotFound
	}
	if err = tx.Model(&Show{Name: name}).Count(&cnt).Error; err != nil {
		tx.Rollback()
		return nil, err
	}
	if cnt > 0 {
		tx.Rollback()
		return nil, ErrShowAlreadyExists
	}

	if show, err = st.createShow(tx, name); err != nil {
		tx.Rollback()
		return nil, err
	}
	var fileIDMapping map[uint64]uint64
	if fileIDMapping, err = st.cloneFiles(tx, name, from); err != nil {
		tx.Rollback()
		os.RemoveAll(st.getShowPath(name))
		return nil, err
	}
	if err = st.clonePlaylists(tx, name, from, fileIDMapping); err != nil {
		tx.Rollback()
		os.RemoveAll(st.getShowPath(name))
		return nil, err
	}

	return show, tx.Commit().Error
}

// this will also remove all files and playlists belonging to the show!
// TODO: fix cascading errors (deletion of files is tried before deletion of playlists
//       which does not work when files are still refrenced by playlists...
func (st *Store) DeleteShow(name string) error {
	if err := st.db.Delete(&Show{Name: name}).Error; err != nil {
		return err
	}
	return os.RemoveAll(st.getShowPath(name))
}

func (st *Store) ListShows() (shows Shows, err error) {
	err = st.db.Find(&shows).Error
	return
}