<?php

namespace Kanboard\Model;

use Exception;
use Kanboard\Core\Base;
use Kanboard\Core\Thumbnail;
use Kanboard\Core\ObjectStorage\ObjectStorageException;

/**
 * Base File Model
 *
 * @package  Kanboard\Model
 * @author   Frederic Guillot
 */
abstract class FileModel extends Base
{
    /**
     * Get the table
     *
     * @abstract
     * @access protected
     * @return string
     */
    abstract protected function getTable();

    /**
     * Define the foreign key
     *
     * @abstract
     * @access protected
     * @return string
     */
    abstract protected function getForeignKey();

    /**
     * Get the path prefix
     *
     * @abstract
     * @access protected
     * @return string
     */
    abstract protected function getPathPrefix();

    /**
     * Fire file creation event
     *
     * @abstract
     * @access protected
     * @param  integer $file_id
     */
    abstract protected function fireCreationEvent($file_id);

    /**
     * Fire file destruction event
     *
     * @abstract
     * @access protected
     * @param  integer $file_id
     */
    abstract protected function fireDestructionEvent($file_id);

    /**
     * Get PicoDb query to get all files
     *
     * @access protected
     * @return \PicoDb\Table
     */
    protected function getQuery()
    {
        return $this->db
            ->table($this->getTable())
            ->columns(
                $this->getTable().'.id',
                $this->getTable().'.name',
                $this->getTable().'.path',
                $this->getTable().'.is_image',
                $this->getTable().'.'.$this->getForeignKey(),
                $this->getTable().'.date',
                $this->getTable().'.user_id',
                $this->getTable().'.size',
                UserModel::TABLE.'.username',
                UserModel::TABLE.'.name as user_name'
            )
            ->join(UserModel::TABLE, 'id', 'user_id')
            ->asc($this->getTable().'.name');
    }

    /**
     * Get a file by the id
     *
     * @access public
     * @param  integer   $file_id    File id
     * @return array
     */
    public function getById($file_id)
    {
        $file = $this->db->table($this->getTable())->eq('id', $file_id)->findOne();
        if ($file) {
            $file['etag'] = md5($file['path']);
        }
        return $file;
    }

    /**
     * Get all files
     *
     * @access public
     * @param  integer   $id
     * @return array
     */
    public function getAll($id)
    {
        $files = $this->getQuery()->eq($this->getForeignKey(), $id)->findAll();
        foreach ($files as &$file) {
            $file['etag'] = md5($file['path']);
        }
        return $files;
    }

    /**
     * Get all images
     *
     * @access public
     * @param  integer   $id
     * @return array
     */
    public function getAllImages($id)
    {
        $images = $this->getQuery()->eq($this->getForeignKey(), $id)->eq('is_image', 1)->findAll();
        foreach ($images as &$image) {
            $image['etag'] = md5($image['path']);
        }
        return $images;
    }

    /**
     * Get all files without images
     *
     * @access public
     * @param  integer   $id
     * @return array
     */
    public function getAllDocuments($id)
    {
        $files = $this->getQuery()->eq($this->getForeignKey(), $id)->eq('is_image', 0)->findAll();
        foreach ($files as &$file) {
            $file['etag'] = md5($file['path']);
        }
        return $files;
    }

    /**
     * Create a file entry in the database
     *
     * @access public
     * @param  integer $foreign_key_id Foreign key
     * @param  string  $name           Filename
     * @param  string  $path           Path on the disk
     * @param  integer $size           File size
     * @return bool|integer
     */
    public function create($foreign_key_id, $name, $path, $size)
    {
        $values = array(
            $this->getForeignKey() => $foreign_key_id,
            'name' => substr($name, 0, 255),
            'path' => $path,
            'is_image' => $this->isImage($name) ? 1 : 0,
            'size' => $size,
            'user_id' => $this->userSession->getId() ?: 0,
            'date' => time(),
        );

        $result = $this->db->table($this->getTable())->insert($values);

        if ($result) {
            $file_id = (int) $this->db->getLastId();
            $this->fireCreationEvent($file_id);
            return $file_id;
        }

        return false;
    }

    /**
     * Remove all files
     *
     * @access public
     * @param  integer   $id
     * @return bool
     */
    public function removeAll($id)
    {
        $file_ids = $this->db->table($this->getTable())->eq($this->getForeignKey(), $id)->asc('id')->findAllByColumn('id');
        $results = array();

        foreach ($file_ids as $file_id) {
            $results[] = $this->remove($file_id);
        }

        return ! in_array(false, $results, true);
    }

    /**
     * Remove a file
     *
     * @access public
     * @param  integer   $file_id    File id
     * @return bool
     */
    public function remove($file_id)
    {
        try {
            $this->fireDestructionEvent($file_id);

            $file = $this->getById($file_id);

            // Only remove files from disk attached to a single task.
            $multiple_tasks_count = $this->db->table($this->getTable())->eq('path', $file['path'])->count();
            if ($multiple_tasks_count === 1) {
                $this->objectStorage->remove($file['path']);

                if ($file['is_image'] == 1) {
                    $this->objectStorage->remove($this->getThumbnailPath($file['path']));
                }
            }

            return $this->db->table($this->getTable())->eq('id', $file['id'])->remove();
        } catch (ObjectStorageException $e) {
            $this->logger->error($e->getMessage());
            return false;
        }
    }

    /**
     * Check if a filename is an image (file types that can be shown as thumbnail)
     *
     * @access public
     * @param  string   $filename   Filename
     * @return bool
     */
    public function isImage($filename)
    {
        switch (get_file_extension($filename)) {
            case 'jpeg':
            case 'jpg':
            case 'png':
            case 'gif':
                return true;
        }

        return false;
    }

    /**
     * Generate the path for a thumbnails
     *
     * @access public
     * @param  string  $key  Storage key
     * @return string
     */
    public function getThumbnailPath($key)
    {
        return 'thumbnails'.DIRECTORY_SEPARATOR.$key;
    }

    /**
     * Generate the path for a new filename
     *
     * @access public
     * @param  integer   $id            Foreign key
     * @param  string    $filename      Filename
     * @return string
     */
    public function generatePath($id, $filename)
    {
        if (is_string($id)) {
            $id = (int) $id;
        }
        if (! is_int($id) || $id <= 0) {
            throw new Exception('Invalid ID provided for file path generation');
        }
        return $this->getPathPrefix().DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR.hash('sha1', $filename.time());
    }

    /**
     * Upload multiple files
     *
     * @access public
     * @param  integer  $id
     * @param  array    $files
     * @return bool
     */
    public function uploadFiles($id, array $files)
    {
        try {
            if (empty($files)) {
                return false;
            }

            foreach (array_keys($files['error']) as $key) {
                $file = array(
                    'name' => $files['name'][$key],
                    'tmp_name' => $files['tmp_name'][$key],
                    'size' => $files['size'][$key],
                    'error' => $files['error'][$key],
                );

                $this->uploadFile($id, $file);
            }

            return true;
        } catch (Exception $e) {
            $this->logger->error($e->getMessage());
            return false;
        }
    }

    /**
     * Upload a file
     *
     * @access public
     * @param  integer $id
     * @param  array   $file
     * @throws Exception
     */
    public function uploadFile($id, array $file)
    {
        if ($file['error'] == UPLOAD_ERR_OK && $file['size'] > 0) {
            $destination_filename = $this->generatePath($id, $file['name']);

            if ($this->isImage($file['name'])) {
                $this->generateThumbnailFromFile($file['tmp_name'], $destination_filename);
            }

            $this->objectStorage->moveUploadedFile($file['tmp_name'], $destination_filename);
            $this->create($id, $file['name'], $destination_filename, $file['size']);
        } else {
            throw new Exception('File not uploaded: '.var_export($file['error'], true));
        }
    }

    /**
     * Handle file upload (base64 encoded content)
     *
     * @access public
     * @param  integer $id
     * @param  string  $originalFilename
     * @param  string  $data
     * @param  bool    $isEncoded
     * @return bool|int
     */
    public function uploadContent($id, $originalFilename, $data, $isEncoded = true)
    {
        try {
            if ($isEncoded) {
                $data = base64_decode($data);
            }

            if (empty($data)) {
                $this->logger->error(__METHOD__.': Content upload with no data');
                return false;
            }

            $destinationFilename = $this->generatePath($id, $originalFilename);
            $this->objectStorage->put($destinationFilename, $data);

            if ($this->isImage($originalFilename)) {
                $this->generateThumbnailFromData($destinationFilename, $data);
            }

            return $this->create(
                $id,
                $originalFilename,
                $destinationFilename,
                strlen($data)
            );
        } catch (ObjectStorageException $e) {
            $this->logger->error($e->getMessage());
            return false;
        }
    }

    /**
     * Generate thumbnail from a blob
     *
     * @access public
     * @param  string   $destination_filename
     * @param  string   $data
     */
    public function generateThumbnailFromData($destination_filename, &$data)
    {
        $blob = Thumbnail::createFromString($data)
            ->resize()
            ->toString();

        $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
    }

    /**
     * Generate thumbnail from a local file
     *
     * @access public
     * @param  string   $uploaded_filename
     * @param  string   $destination_filename
     */
    public function generateThumbnailFromFile($uploaded_filename, $destination_filename)
    {
        $blob = Thumbnail::createFromFile($uploaded_filename)
            ->resize()
            ->toString();

        $this->objectStorage->put($this->getThumbnailPath($destination_filename), $blob);
    }
}
