import org.codehaus.groovy.grails.commons.ConfigurationHolder
import org.apache.commons.lang.WordUtils

/**
* Provides a service class for the InventoryItem domain class.
*/
class InventoryItemService {

    boolean transactional = false

    def createDataService

    def sessionFactory
    def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP

    /**
    * Prepare the data for the show view.
    * The result can be used to easily construct the model for the show view.
    * @param params The incoming params as normally passed to the show view
    * primarily including the id of the inventoryItem.
    * @returns A map containing result.error, if any error, otherwise result.inventoryItemInstance.
    */
    def show(params) {
        def result = [:]

        def fail = { Map m ->
            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
            return result
        }

        result.inventoryItemInstance = InventoryItem.get( params.id )

        if(!result.inventoryItemInstance)
            return fail(code:"default.not.found")

        def p = [:]

        if(params.paginate == "purchases") {
            params.showTab = "showPurchasingTab"
            p.max = Math.min(params.max?.toInteger() ?: 10, 100)
            p.offset = params.offset?.toInteger() ?: 0
            p.sort = params.sort ?: null
            p.order = params.order ?: null
        }
        else {
            p.max = 10
            p.offset = 0
        }

        result.inventoryItemPurchasesTotal = InventoryItemPurchase.countByInventoryItem(result.inventoryItemInstance)

        result.inventoryItemPurchases = InventoryItemPurchase.withCriteria {
                eq("inventoryItem", result.inventoryItemInstance)
                maxResults(p.max)
                firstResult(p.offset)
                // Sorting:
                // Default is to sort by order number then id.
                // When a sortable column is clicked then we sort by that.
                // If the sortable column clicked is order number then we add id as the second sort.
                if(p.sort && p.order) {
                    order(p.sort, p.order)
                    if(p.sort == "purchaseOrderNumber") 
                        order('id', 'asc')
                }
                else {
                    order('purchaseOrder', 'desc')
                    order('id', 'asc')
                }
            }

        result.showTab = [:]
        switch (params.showTab) {
            case "showMovementTab":
                result.showTab.movement =  new String("true")
                break
            case "showPurchasingTab":
                result.showTab.purchasing =  new String("true")
                break
            default:
                result.showTab.inventory = new String("true")
        }

        p.max = result.inventoryMovementListMax = 10
        p.offset = 0
        p.order = "desc"
        p.sort = "id"
        result.inventoryMovementList = InventoryMovement.findAllByInventoryItem(result.inventoryItemInstance, p)
        result.inventoryMovementListTotal = InventoryMovement.countByInventoryItem(result.inventoryItemInstance)


        // Success.
        return result

    } // end show()

    def delete(params) {
        InventoryItem.withTransaction { status ->
            def result = [:]

            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemInstance && m.field)
                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
                return result
            }

            result.inventoryItemInstance = InventoryItem.get(params.id)

            if(!result.inventoryItemInstance)
                return fail(code:"default.not.found")

            if(result.inventoryItemInstance.inventoryMovements)
                return fail(code:"inventoryMovement.still.associated")

            try {
                result.inventoryItemInstance.delete(flush:true)
                return result //Success.
            }
            catch(org.springframework.dao.DataIntegrityViolationException e) {
                return fail(code:"default.delete.failure")
            }

        } //end withTransaction
    } // end delete()

    def edit(params) {
        def result = [:]
        def fail = { Map m ->
            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
            return result
        }

        result.inventoryItemInstance = InventoryItem.get(params.id)

        if(!result.inventoryItemInstance)
            return fail(code:"default.not.found")

        // Success.
        return result
    }

    def update(params) {
        InventoryItem.withTransaction { status ->
            def result = [:]

            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemInstance && m.field)
                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
                return result
            }

            result.inventoryItemInstance = InventoryItem.get(params.id)

            if(!result.inventoryItemInstance)
                return fail(code:"default.not.found")

            // Optimistic locking check.
            if(params.version) {
                if(result.inventoryItemInstance.version > params.version.toLong())
                    return fail(field:"version", code:"default.optimistic.locking.failure")
            }

            result.inventoryItemInstance.properties = params
            result.inventoryItemInstance.setAlternateSuppliersFromCheckBoxList(params.alternateSuppliers)
            result.inventoryItemInstance.setSpareForFromCheckBoxList(params.spareFor)

            // Fetch to prevent lazy initialization error.
            result.inventoryItemInstance.unitOfMeasure

            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
                return fail(code:"default.update.failure")

            // Success.
            return result

        } //end withTransaction
    }  // end update()

    def create(params) {
        def result = [:]
        def fail = { Map m ->
            result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
            return result
        }

        result.inventoryItemInstance = new InventoryItem()
        result.inventoryItemInstance.properties = params

        // success
        return result
    }

    def save(params) {
        InventoryItem.withTransaction { status ->
            def result = [:]

            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemInstance && m.field)
                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: ["InventoryItem", params.id] ]
                return result
            }

            result.inventoryItemInstance = new InventoryItem(params)
            result.inventoryItemInstance.setAlternateSuppliersFromCheckBoxList(params.alternateSuppliers)
            result.inventoryItemInstance.setSpareForFromCheckBoxList(params.spareFor)

            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
                return fail(code:"default.create.failure")

            // success
            return result

        } //end withTransaction
    }

    /**
    * Save an inventory item picture.
    * @param params An object or map containing at least the inventoryItem ID.
    * @param pictureSource A supported source to get the picture image from.
    * Supported sources:
    * HttpServletRequest e.g: 'request' var from controller to run getFile('file') against.
    * ServletContextResource e.g: grailsApplication.mainContext.getResource('images/logo.png')
    * File e.g: new File('picture.jpg')
    */
    def savePicture(params, pictureSource) {
        InventoryItem.withTransaction { status ->
            def result = [:]

            def kByteMultiplier = 1000

            def fail = { Map m ->
                status.setRollbackOnly()
                if(result.inventoryItemInstance && m.field)
                    result.inventoryItemInstance.errors.rejectValue(m.field, m.code)
                result.error = [ code: m.code, args: m.args ?: ["InventoryItem", params.id] ]
                return result
            }

            result.inventoryItemInstance = InventoryItem.get(params.id)

            if(!result.inventoryItemInstance)
                return fail(code:"default.not.found")

            // Optimistic locking check.
            if(params.version) {
                if(result.inventoryItemInstance.version > params.version.toLong())
                    return fail(field:"version", code:"default.optimistic.locking.failure")
            }

            if(result.inventoryItemInstance.picture)
                return fail(field:"picture", code:"inventory.item.already.has.picture")

            // Declare some more variables, since we appear to have most of what we need.
            def picture = new Picture(inventoryItem: result.inventoryItemInstance)
            def imaging = new Imaging()
            def images = null
            def pictureFile
            def pictureFileName = ''
            def pictureInputStream

            // Check the supplied pictureSource and get the inputStream.
            if(pictureSource instanceof javax.servlet.http.HttpServletRequest) {
                def multiPartFile = pictureSource.getFile('file')
                pictureFileName = multiPartFile.originalFilename

                if(!multiPartFile || multiPartFile.isEmpty())
                    return fail(code: "default.file.not.supplied")

                if (multiPartFile.getSize() > Image.MAX_SIZE)
                    return fail(code: "default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])

                pictureInputStream = multiPartFile.inputStream
            }
            else if(pictureSource instanceof org.springframework.web.context.support.ServletContextResource) {
                pictureFile = pictureSource.getFile()
                pictureFileName = pictureFile.name

                if ( !pictureFile.isFile() || (pictureFile.length() == 0) )
                    return fail(code:"default.file.not.supplied")

                if (pictureFile.length() > Image.MAX_SIZE)
                    return fail(code:"default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])

                pictureInputStream = pictureSource.inputStream
            }
            else if(pictureSource instanceof File) {
                pictureFile = pictureSource
                pictureFileName = pictureFile.name

                if ( !pictureFile.isFile() || (pictureFile.length() == 0) )
                    return fail(code:"default.file.not.supplied")

                if (pictureFile.length() > Image.MAX_SIZE)
                    return fail(code:"default.file.over.max.size", args: [Image.MAX_SIZE/kByteMultiplier, "kB"])

                pictureInputStream = new FileInputStream(pictureSource)
            }
            else {
                    return fail(code:"inventory.item.picture.source.not.supported")
            }

            // Create the Images.
            try {
                images = imaging.createAll(result.inventoryItemInstance, picture, pictureInputStream)
                // Ensure the stream is closed.
                pictureInputStream.close()
            }
            catch(Exception ex) {
                log.error("picture save", ex)
                // Ensure the stream is closed.
                pictureInputStream.close()
                return fail(code:"inventory.item.picture.file.unrecognised", args: [pictureFileName])
            }

            // Add images to picture.
            images.each { image ->
                picture.addToImages(image)
            }

            // Save picture.
            if(picture.hasErrors() || !picture.save())
                return fail(code:"default.create.failure", args: ["Picture"])

            result.inventoryItemInstance.picture = picture

            // Save inventoryItem.
            if(result.inventoryItemInstance.hasErrors() || !result.inventoryItemInstance.save())
                return fail(code:"default.create.failure")

            // success
            return result

        } // end withTransaction
    } // savePicture

    /**
    * Import inventory pictures from an uploaded zip file or picture.
    * @param request The http request to run getFile against.
    * Get file should return a zip format file containing the inventory item pictures or a picture file.
    */
    def importInventoryItemPictures(request) {
            def result = [:]

            def kByteMultiplier = 1000
            def mByteMultiplier = 1000 * kByteMultiplier
            def fileMaxSize = 100 * mByteMultiplier

            def fail = { Map m ->
                result.error = [ code: m.code, args: m.args ]
                return result
            }

            // Get file from request.
            def multiPartFile = request.getFile('file')
            def uploadedFileName = multiPartFile.originalFilename

            if(!multiPartFile || multiPartFile.isEmpty())
                return fail(code: "default.file.not.supplied")

            if (multiPartFile.getSize() > fileMaxSize)
                return fail(code: "default.file.over.max.size", args: [fileMaxSize/mByteMultiplier, "MB"])

            // Check and create import dir.
            def dir = new File(ConfigurationHolder.config.globalDirs.tempInventoryItemPicturesDirectory)

            if(!dir.exists())
                dir.mkdirs()

            if(!dir.isDirectory()) {
                return fail(code:'inventoryItemPictures.import.failure.no.directory')
            }

            // Write file to disk.
            def diskFile = new File(dir.absolutePath + File.separator + uploadedFileName)
            multiPartFile.transferTo(diskFile)

            // File patterns
            def zipFilePattern = ~/[^\s].*(\.(?i)(zip))$/
            def pictureFilePattern = ~/[^\s].*(\.(?i)(jpg|png|gif|bmp))$/

            // If file claims to be a zip file then try using ant to unzip.
            if(diskFile.name.matches(zipFilePattern)) {
                def ant = new AntBuilder()
                try {
                    ant.unzip(  src: diskFile.absolutePath,
                                        dest: dir.absolutePath,
                                        overwrite:"true" )
                }
                catch(e) {
                    log.error e
                    return fail(code:'inventoryItemPictures.import.failure.to.unzip')
                }
            }

            // Recurse through dir building list of pictureFiles.
            def pictureFiles = []
            dir.eachFileMatch(pictureFilePattern) {
                pictureFiles << it
            }

            dir.eachDirRecurse { subDir ->
                subDir.eachFileMatch(pictureFilePattern) {
                    pictureFiles << it
                }
            }

            pictureFiles.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }

            // Find inventoryItems by name of picture and call savePicture.
            def inventoryItemInstance
            def itemName
            def savePictureResult
            def pictureCount = 0
            def picturesSavedCount = 0

            // Turn off index mirroring.
            createDataService.stopSearchableIndex()

            for(pictureFile in pictureFiles) {
                pictureCount++

                if(pictureCount % 10 == 0) {
                    cleanUpGorm()
                }

                itemName = WordUtils.capitalize(pictureFile.name[0..-5])
                inventoryItemInstance = InventoryItem.findByName(itemName)
                if(!inventoryItemInstance) {
                    log.warn 'InventoryItem not found with name: ' + itemName
                    continue
                }
                if(inventoryItemInstance.picture) {
                    log.warn 'InventoryItem already has picture: ' + itemName
                    continue
                }
                savePictureResult = savePicture(inventoryItemInstance, pictureFile)
                if(savePictureResult.error)
                    log.error savePictureResult.error
                else {
                    picturesSavedCount++
                    log.info 'InventoryItem picture saved: ' + itemName
                }
            }

            // Start mirroring again and rebuild index.
            createDataService.startSearchableIndex()

            log.info 'InventoryItem pictures saved: ' + picturesSavedCount
            log.info 'InventoryItem pictures total: ' + pictureCount

            // Cleanup.
            dir.eachFile() {
                if(it.isDirectory())
                    it.deleteDir()
                else
                    it.delete()
            }

            // Success.
            return result

    } // importInventoryItemPictures

    /**
    * This cleans up the hibernate session and a grails map.
    * For more info see: http://naleid.com/blog/2009/10/01/batch-import-performance-with-grails-and-mysql/
    * The hibernate session flush is normal for hibernate.
    * The map is apparently used by grails for domain object validation errors.
    * A starting point for clean up is every 100 objects.
    */
    def cleanUpGorm() {
        def session = sessionFactory.currentSession
        session.flush()
        session.clear()
        propertyInstanceMap.get().clear()
    }

} // end class
