# -*- coding: UTF-8 -*-
#    This file is part of arena.
#
#    arena 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, at your
#    option, any later version.
#
#    arena is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABLITY
#    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 arena.  If not, see <http://www.gnu.org/licenses/>.

## @file
#  This file defines a class to manage a single arena attachment.

from Library.DataSource.DataRows import ADataRowWithExpandingFields, \
    ADataRowError

from Library.UI.Channels import chMetaItemsSeparator, \
    chMetaKeysValuesSeparator
from Library.String.Utilities import String, Time, sDefaultItemsDelimiter
from Library.IO.StringStreams import AStringStream
from Plugins.User.Subscriptions import Right

from Local.Configuration import Site
import mimetypes, os, shutil

## The name of the query key which specifies a file that is uploaded for 
#  attachment
atFileQueryKey = "file"

## The error message used when a required attachment parameter is not provided
atErrorMissingParameter = "the {parameter} must be specified"
## The error message used when a file upload fails
atErrorFileUpload = "the process of uploading the file ended unexpectedly"
## The error message used when an uploaded file exceeds the maximum file size
atErrorFileSize = "attachments are limited to {size} or less"
## The error message used when failing to manipulated a record for an attachment
atErrorChangeRecord = "failed to {} attachment data"
## The error message used when the user does not have the right to upload
atErrorRights = "you lack the rights required to {} attachments"

## Used to match a MIME type with an attachment category
atMimeTypeToCategory = {
    "application/msword": "documents",
    "application/pdf": "documents",
    "application/x-tar": "archives",
    "application/x-stuffit": "archives"
}    

## Represents an error that may occur with an attachment
class AnArenaAttachmentError(ADataRowError):
    pass
    
## Represents a single attachment in arena
class AnArenaAttachment(ADataRowWithExpandingFields):
    ## Constructor
    def __init__(Self, fieldsValues=None, **params):
        # Establish our expanding fields
        params.update({
            "expand": {
                "keywords": (list, sDefaultItemsDelimiter, True),
                "meta": (dict, chMetaKeysValuesSeparator, chMetaItemsSeparator)
            },
            "category": Site.Attachments.category
        })

        ADataRowWithExpandingFields.__init__(Self, fieldsValues, **params)
        
        if "Mediator" in Self._params:
            Self._Mediator = Self._params["Mediator"]
            
            if Self._Source is None:
                Self._Source = Self._Mediator.DataSource()
                
            del Self._params["Mediator"]
            
        if "User" in Self._params:
            Self._User = Self._params["User"]
            del Self._params["User"]
            
        if "Channel" in Self._params:
            Self._Channel = Self._params["Channel"]
            del Self._params["Channel"]
                    
    ## Initializer
    def init(Self):
        result = ADataRowWithExpandingFields.init(Self)
        
        if result:
            # Establish private attributes
            # The mediator driving display of content
            Self._Mediator = None
            # The user posting the attachment
            Self._User = None
            # The channel to which the attachment is posted
            Self._Channel = None
            
        return result
                    
    ## Flush changes made to the attachment record back to the data source
    def flush(Self, *criteria):
        if len(Self._changes) > 0:
            cri = list(criteria)
            if len(cri) == 0:
                cri.append(("{field:id} = {}", Self["id"]))
                
            # Remove the "id" field since it is immutable and the data source
            # will complain if we attempt to update it
            aid = Self._fieldsValues["id"]          
            del Self._fieldsValues["id"]
            
            # Condense keywords
            Self._fieldsValues["keywords"] = String.implode(Self._keywords)

            # Condense metadata
            Self._fieldsValues["meta"] = \
              String.implode(Self._meta, chMetaItemsSeparator, 
                  chMetaKeysValuesSeparator)
                  
            ADataRow.flush(Self, *cri)    
            
            # Restore the "id" field
            Self._fieldsValues["id"] = aid      
            
    ## Fetch the attachment record from the data source.
    def fetch(Self, **params):
        result = ADataRowWithExpandingFields.fetch(Self, **params)
                   
        # Expand channel metadata
        if len(Self["meta"]) > 0:
            Self._meta = String.explode(Self["meta"], dict, 
                chMetaItemsSeparator, chMetaKeysValuesSeparator)
                
        return result  
        
    ## Categorize the specified MIME type
    def categorize(Self, mimeType):
        result = "other"
        
        mtype = mimeType.lower()
        
        # Is it an image?
        if "image/" in mtype:
            result = "images"
            
        # A document?
        elif "text/" in mtype:
            result = "documents"
            
        # An archive?
        elif "zip" in mtype:
            result = "archives"
            
        elif mtype in atMimeTypeToCategory:
            result = atMimeTypeToCategory[mtype]
            
        return result            
        
    ## Copy the specified file object to the named destination
    def copyFile(Self, f, dest):
        outputFile = None
        
        try:
            outputFile = open(dest, "wb")
            shutil.copyfileobj(f, outputFile)
            
        finally:
            if outputFile is not None:
                outputFile.close()
        
    ## Update the attachment data from the supplied client request
    def updateFromRequest(Self, ThisRequest):
        # Get the attachment name
        Self["attachmentName"] = ThisRequest["attachmentName"].value
                
        # Get the MIME type of the attached file
        Self["mimeType"] = ThisRequest[atFileQueryKey].type
        # Match the MIME type to a category
        Self["category"] = Self.categorize(Self["mimeType"])
        
        # Set the attachment icon
        Self["icon"] = "/media/icons/attachment/" + Self["mimeType"] + ".png"
        # Set the attachment storage location
        Self["localPath"] = Site.Attachments.storage["local"] + "/" + \
            Self["category"] + "/" + Self["attachmentName"] + \
            mimetypes.guess_extension(Self["mimeType"], True)  
        
        Self["clientPath"] = Site.Attachments.storage["client"] + "/" + \
            Self["category"] + "/" + Self["attachmentName"] + \
            mimetypes.guess_extension(Self["mimeType"], True)
            
        # Set the name of the user uploading the file
        Self["editor"] = Self._User.name()
        Self["published"] = Time.now()        
            
        Self["ip"] = ThisRequest.Remote.address
        
        if "description" in ThisRequest:
            Self["description"] = ThisRequest["description"].value
        
        if "keywords" in Self._Mediator.Request:
            Self["keywords"].append(
                *String.explode(ThisRequest["keywords"].value)
        )
            
    ## Validate and retrieve an uploaded file
    def uploadedFile(Self, ThisRequest):
        result = None
        
        # The file needs to have uploaded successfully
        if ThisRequest[atFileQueryKey].done == -1:
            Self.error(AnArenaAttachmentError(atErrorFileUpload))
        
        result = Self._Mediator.Request[atFileQueryKey].file
                
        # Check the size of the attached file
        result.seek(0, 2)
        Self["fileSize"] = result.tell()
        result.seek(0)
        
        if Self["fileSize"] > Site.Attachments.sizeLimit:
            Self.error(AnArenaAttachmentError(atErrorFileSize, 
                size=String.fileSize(Site.Attachments.sizeLimit, True)))
                
        return result
        
    ## Upload an attachment
    def upload(Self):
        # Ensure the user has the right to upload an attachment
        if Self._User is None:
            return
            
        if Self._User.has(Right.toPost, Self._Channel.name()) == False:
            Self.error(AnArenaAttachmentError(atErrorRights, "upload"))
            
        # We need a mediator in order to access the client request
        if Self._Mediator is None:
            return   
            
        # The client needs to have attached a file
        if atFileQueryKey not in Self._Mediator.Request:
            return
            
        if "attachmentName" not in Self._Mediator.Request:
            Self.error(AnArenaAttachmentError(atErrorMissingParameter, 
                parameter="attachment name"))                
            
        tempFile = Self.uploadedFile(Self._Mediator.Request)
                
        Self.updateFromRequest(Self._Mediator.Request)
        Self["author"] = Self._User.name()
        Self["created"] = Time.now()        
                    
        # Insert the attachment record
        Self.readOnly = False
        if Self.insert() > 0:
            # Copy the file to its new location
            Self.copyFile(tempFile, Self["localPath"])
            
        else:
            Self.error(AnArenaAttachmentError(atErrorChangeRecord, "store")) 
            
    ## Edit an attachment
    def edit(Self):
        # Ensure that the user has the appropriate rights
        if Self._User is None:
            return
            
        if Self._User.has(Right.toEdit, Self._Channel.name()) == False:
            Self.error(AnArenaAttachmentError(atErrorRights, "edit"))

        # The client needs to have attached a file
        if atFileQueryKey not in Self._Mediator.Request:
            return
            
        if "attachmentName" not in Self._Mediator.Request:
            Self.error(AnArenaAttachmentError(atErrorMissingParameter, 
                parameter="attachment name"))  
                
        newFile = Self.uploadedFile(Self._Mediator.Request)
                        
        Self.updateFromRequest(Self._Mediator.Request)
        
        # Update the attachment record
        Self.readOnly = False
        if Self.update() > 0:
            # Copy the file to its new location
            Self.copyFile(newFile, Self["localPath"])
            
        else:
            Self.error(AnArenaAttachmentError(atErrorChangeRecord, "update"))
            
    ## Delete an attachment
    def delete(Self):
        # Ensure that the user has the appropriate rights
        if Self._User is None:
            return
            
        if Self._User.has(Right.toDelete, Self._Channel.name()) == False:
            Self.error(AnArenaAttachmentError(atErrorRights, "delete"))
        
        try:
            # Delete the file first
            os.unlink(Self["localPath"])
          
            # Delete the record
            if ADataRowWithExpandingFields.delete(Self) > 0:
                # Prevent attempted changes to a deleted record
                Self.readOnly = True
                Self["description"] = "This attachment has been deleted."
                Self["mimeType"] = "None"
                Self["icon"] = ""
            
            else:
                Self.error(AnArenaAttachmentError(atErrorChangeRecord, "delete"))
              
        except Exception,Err:
            Self.error(Err)
        
    # Properties ---------------------------------------------------------------
    ## Retrieve the name of the attachment
    def name(Self):
        if "attachmentName" in Self:
            return Self["attachmentName"]
            
        return ADataRowWithExpandingFields.name(Self)
        
    ## Retrieve the attachment icon
    def icon(Self):
        if String.isEmpty(Self["icon"]) == False:
            return Self._Mediator.path("client", Self["icon"])
            
        return None
        
    ## Retrieve a URL that will allow users to upload a new attachment
    def uploadLink(Self):
        return Self._Mediator.url(channel=Site.Channels.attachments,
            view="site/search/attachments/results",
            title=Self["attachmentName"])
