import grails.util.GrailsUtil
import au.com.bytecode.opencsv.CSVWriter
import au.com.bytecode.opencsv.CSVReader
import org.apache.commons.lang.WordUtils

/**
 * Provides some csv import/export methods.
 * Requires the opencsv jar to be available which is included in the grails-export plugin.
 */
class InventoryCsvService {

    boolean transactional = false

    def dateUtilService

    def g = new org.codehaus.groovy.grails.plugins.web.taglib.ApplicationTagLib()

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

    /**
    * Import inventory creating items as required.
    * @param request The http request to run getFile against.
    * Get file should return a csv format file containing the inventory as per template.
    */
    def importInventory(request) {
        InventoryItem.withTransaction { status ->
            def result = [:]

            def kByteMultiplier = 1000
            def fileMaxSize = 800 * kByteMultiplier
            def logFileLink = g.link(controller: "appCore", action: "appLog") {"log"}

            def multiPartFile = request.getFile('file')

            InputStreamReader sr = new InputStreamReader(multiPartFile.inputStream)
            CSVReader reader = new CSVReader(sr)

            def fail = { Map m ->
                status.setRollbackOnly()
                reader.close()
                result.error = [ code: m.code, args: m.args ]
                return result
            }

            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/kByteMultiplier, "kB"])

            def line = []
            def lineNumber = 0
            def maxNumberOfColumns = 25
            def inventoryParams = [:]
            def inventoryProperties = ["name", "description", "comment", "unitsInStock", "reorderPoint", "recommendedReorderPoint",
                                                        "unitOfMeasure", "estimatedUnitPriceAmount", "estimatedUnitPriceCurrency",
                                                        "enableReorder", "inventoryLocation", "inventoryStore", "site",
                                                        "inventoryGroup", "inventoryType", "averageDeliveryTime", "averageDeliveryPeriod",
                                                        "suppliersPartNumber", "preferredSupplier", "alternateSuppliers",
                                                        "manufacturersPartNumber", "preferredManufacturer", "alternateManufacturers",
                                                        "alternateItems", "spareFor"]

            def siteInstance
            def alternateSupplierInstance
            def preferredSupplierInstance
            def supplierTypeInstance
            def supplierTypeUnknown = SupplierType.get(1)
            def spareForInstance
            def alternateItemInstance
            def preferredManufacturerInstance
            def alternateManufacturerInstance
            def manufacturerTypeInstance
            def manufacturerTypeUnknown = ManufacturerType.get(1)
            def inventoryTypeInstance
            def unitOfMeasureInstance
            def inventoryGroupInstance
            def inventoryItemInstance
            def inventoryStoreInstance
            def inventoryLocationInstance
            def averageDeliveryPeriodInstance

            def tempPreferredSupplierItemAndType = ''
            def tempPreferredSupplierItem = ''
            def tempPreferredSupplierType = ''

            def tempAlternateSuppliers = []
            def tempSupplierItem = ''
            def tempSupplierType = ''
            def tempSupplierItemAndType = []

            def tempPreferredManufacturerItemAndType = ''
            def tempPreferredManufacturerItem = ''
            def tempPreferredManufacturerType = ''

            def tempAlternateManufacturers = []
            def tempManufacturerItem = ''
            def tempManufacturerType = ''
            def tempManufacturerItemAndType = []

            def tempSpareFor = []
            def tempAlternateItems = []

            def nextLine = {
                    line = reader.readNext()
                    lineNumber ++
                    log.info "Processing line: " + lineNumber
            }

            def parseInputList = {
                if( (it == null) || (it.trim() == '') ) return []
                return it.split(";").collect{it.trim()}
            }

            def parseItemAndType = {
                return it.split("@").collect{it.trim()}
            }

            // Get first line.
            nextLine()

            // Check for header line 1.
            if(line != templateHeaderLine1) {
                log.error "Failed to find header line 1. "
                log.error "Required: " + templateHeaderLine1.toString()
                log.error "Supplied: " + line.toString()
                return fail(code: "default.file.no.header")
            }

            log.info "Header line found."

            // Prepare the first body line.
            nextLine()

            // Primary loop.
            while(line) {

                if(line.size() > maxNumberOfColumns) {
                    log.error "Too many columns on line: " + lineNumber
                    return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                }

                // Ignore comment lines.
                if(line.toString().toLowerCase().contains("comment")) {
                    log.info "Comment line found."
                    nextLine()
                    continue
                }

                // Ignore example lines.
                if(line.toString().toLowerCase().contains("example")) {
                    log.info "Example line found."
                    nextLine()
                    continue
                }

                // Parse the line into the params map.
                inventoryParams = [:]
                line.eachWithIndex { it, j ->
                    inventoryParams."${inventoryProperties[j]}" = it.trim()
                }

                // Debug
                log.debug " Supplied params: "
                log.debug inventoryParams

                // Ignore blank lines.
                if(inventoryParams.name == '') {
                    log.info "No name found."
                    nextLine()
                    continue
                }

                /** Prepare the params and create supporting items as required. */

                // Site
                inventoryParams.site = WordUtils.capitalize(inventoryParams.site)
                siteInstance = Site.findByName(inventoryParams.site)
                if(!siteInstance) {
                    siteInstance = new Site(name: inventoryParams.site)
                    if(!siteInstance.save()) {
                        log.error "Failed to create site on line: " + lineNumber
                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                    }
                }

                // InventoryStore
                inventoryParams.inventoryStore = WordUtils.capitalizeFully(inventoryParams.inventoryStore)
                inventoryStoreInstance = InventoryStore.findByName(inventoryParams.inventoryStore)
                if(!inventoryStoreInstance) {
                    inventoryStoreInstance = new InventoryStore(name: inventoryParams.inventoryStore,
                                                                                                site: siteInstance)
                    if(!inventoryStoreInstance.save()) {
                        log.error "Failed to create inventory store on line: " + lineNumber
                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                    }
                }

                // InventoryLocation
                inventoryParams.inventoryLocation = WordUtils.capitalize(inventoryParams.inventoryLocation)
                inventoryLocationInstance = InventoryLocation.findByName(inventoryParams.inventoryLocation)
                if(!inventoryLocationInstance) {
                    inventoryLocationInstance = new InventoryLocation(name: inventoryParams.inventoryLocation,
                                                                                                        inventoryStore: inventoryStoreInstance)
                    if(!inventoryLocationInstance.save()) {
                        log.error "Failed to create inventory location on line: " + lineNumber
                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                    }
                }

                // InventoryGroup
                inventoryParams.inventoryLocation = WordUtils.capitalizeFully(inventoryParams.inventoryLocation)
                inventoryGroupInstance = InventoryGroup.findByName(inventoryParams.inventoryGroup)
                if(!inventoryGroupInstance) {
                    inventoryGroupInstance = new InventoryGroup(name: inventoryParams.inventoryGroup)
                    if(!inventoryGroupInstance.save()) {
                        log.error "Failed to create inventory group on line: " + lineNumber
                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                    }
                }

                // InventoryType
                inventoryParams.inventoryType = WordUtils.capitalizeFully(inventoryParams.inventoryType)
                inventoryTypeInstance = InventoryType.findByName(inventoryParams.inventoryType)
                if(!inventoryTypeInstance) {
                    inventoryTypeInstance = new InventoryType(name: inventoryParams.inventoryType)
                    if(!inventoryTypeInstance.save()) {
                        log.error "Failed to create inventory type on line: " + lineNumber
                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                    }
                }

                // UnitOfMeasure.
                unitOfMeasureInstance = UnitOfMeasure.findByName(inventoryParams.unitOfMeasure)
                if(!unitOfMeasureInstance) {
                    unitOfMeasureInstance = new UnitOfMeasure(name: inventoryParams.unitOfMeasure)
                    if(!unitOfMeasureInstance.save()) {
                        log.error "Failed to create unit of measure on line: " + lineNumber
                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                    }
                }

                // AverageDeliveryPeriod.
                if(inventoryParams.averageDeliveryPeriod) {
                    averageDeliveryPeriodInstance = Period.findByPeriod(inventoryParams.averageDeliveryPeriod)
                    if(!averageDeliveryPeriodInstance) {
                        log.error "Failed, not a valid delivery period on line: " + lineNumber
                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                    }
                }

                // Preferred Manufacturer
                if(inventoryParams.preferredManufacturer) {
                    tempPreferredManufacturerItemAndType = parseItemAndType(inventoryParams.preferredManufacturer)
                    tempPreferredManufacturerItem = WordUtils.capitalize(tempPreferredManufacturerItemAndType[0])

                    preferredManufacturerInstance = Manufacturer.findByName(tempPreferredManufacturerItem)
                    if(!preferredManufacturerInstance) {

                        // Manufacturer Type.
                        if(tempPreferredManufacturerItemAndType.size == 2) {
                            tempPreferredManufacturerType = WordUtils.capitalize(tempPreferredManufacturerItemAndType[1])
                            manufacturerTypeInstance = ManufacturerType.findByName(tempPreferredManufacturerType)
                        }
                        else
                            manufacturerTypeInstance = manufacturerTypeUnknown
                        if(!manufacturerTypeInstance) {
                            log.error "Failed to find preferred manufacturer type on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }

                        preferredManufacturerInstance = new Manufacturer(name: tempPreferredManufacturerItem,
                                                                                                            manufacturerType: manufacturerTypeInstance)
                        if(!preferredManufacturerInstance.save()) {
                            log.error "Failed to create preferred manufacturer on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }
                    }
                }
                else
                    preferredManufacturerInstance = null

                // Alternate Manufacturers.
                tempAlternateManufacturers = parseInputList(inventoryParams.alternateManufacturers)
                inventoryParams.alternateManufacturers = []

                for(tempManufacturer in tempAlternateManufacturers) {
                    tempManufacturerItemAndType = parseItemAndType(tempManufacturer)
                    tempManufacturerItem = WordUtils.capitalizeFully(tempManufacturerItemAndType[0])

                    alternateManufacturerInstance = Manufacturer.findByName(tempManufacturerItem)
                    if(!alternateManufacturerInstance) {

                        // ManufacturerType.
                        if(tempManufacturerItemAndType.size == 2) {
                            tempManufacturerType = WordUtils.capitalize(tempManufacturerItemAndType[1])
                            manufacturerTypeInstance = ManufacturerType.findByName(tempManufacturerType)
                        }
                        else
                            manufacturerTypeInstance = manufacturerTypeUnknown
                        if(!manufacturerTypeInstance) {
                            log.error "Failed to find manufacturer type on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }

                        alternateManufacturerInstance = new Manufacturer(name: tempManufacturerItem,
                                                                                                manufacturerType: manufacturerTypeInstance)
                        if(!alternateManufacturerInstance.save()) {
                            log.error "Failed to create manufacturers on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }
                    }

                    inventoryParams.alternateManufacturers.add(alternateManufacturerInstance)
                }

                // Preferred Supplier
                if(inventoryParams.preferredSupplier) {
                    tempPreferredSupplierItemAndType = parseItemAndType(inventoryParams.preferredSupplier)
                    tempPreferredSupplierItem = WordUtils.capitalize(tempPreferredSupplierItemAndType[0])

                    preferredSupplierInstance = Supplier.findByName(tempPreferredSupplierItem)
                    if(!preferredSupplierInstance) {

                        // SupplierType.
                        if(tempPreferredSupplierItemAndType.size == 2) {
                            tempPreferredSupplierType = WordUtils.capitalize(tempPreferredSupplierItemAndType[1])
                            supplierTypeInstance = SupplierType.findByName(tempPreferredSupplierType)
                        }
                        else
                            supplierTypeInstance = supplierTypeUnknown
                        if(!supplierTypeInstance) {
                            log.error "Failed to find preferred supplier type on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }

                        preferredSupplierInstance = new Supplier(name: tempPreferredSupplierItem,
                                                                                            supplierType: supplierTypeInstance)
                        if(!preferredSupplierInstance.save()) {
                            log.error "Failed to create preferred supplier on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }
                    }
                }
                else
                    preferredSupplierInstance = null

                // Alternate Suppliers.
                tempAlternateSuppliers = parseInputList(inventoryParams.alternateSuppliers)
                inventoryParams.alternateSuppliers = []

                for(tempSupplier in tempAlternateSuppliers) {
                    tempSupplierItemAndType = parseItemAndType(tempSupplier)
                    tempSupplierItem = WordUtils.capitalizeFully(tempSupplierItemAndType[0])

                    alternateSupplierInstance = Supplier.findByName(tempSupplierItem)
                    if(!alternateSupplierInstance) {

                        // SupplierType.
                        if(tempSupplierItemAndType.size == 2) {
                            tempSupplierType = WordUtils.capitalize(tempSupplierItemAndType[1])
                            supplierTypeInstance = SupplierType.findByName(tempSupplierType)
                        }
                        else
                            supplierTypeInstance = supplierTypeUnknown
                        if(!supplierTypeInstance) {
                            log.error "Failed to find alternate supplier type on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }

                        alternateSupplierInstance = new Supplier(name: tempSupplierItem,
                                                                            supplierType: supplierTypeInstance)
                        if(!alternateSupplierInstance.save()) {
                            log.error "Failed to create alternate suppliers on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }
                    }

                    inventoryParams.alternateSuppliers.add(alternateSupplierInstance)
                }

                // AlternateItems.
                tempAlternateItems = parseInputList(inventoryParams.alternateItems)
                inventoryParams.alternateItems = []

                for(tempAlternateItem in tempAlternateItems) {
                    tempAlternateItem = WordUtils.capitalize(tempAlternateItem)
                    alternateItemInstance = InventoryItem.findByName(tempAlternateItem)
                    if(!alternateItemInstance) {
                        alternateItemInstance = new InventoryItem(name: tempAlternateItem,
                                                                                                description: "Generated from alternateItems during import, details may not be correct.",
                                                                                                reorderPoint: 0,
                                                                                                inventoryGroup: inventoryGroupInstance,
                                                                                                inventoryType: inventoryTypeInstance,
                                                                                                unitOfMeasure: unitOfMeasureInstance,
                                                                                                inventoryLocation: inventoryLocationInstance)
                        if(!alternateItemInstance.save()) {
                            log.error "Failed to create alternateItems on line: " + lineNumber
                            return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                        }
                    }

                    inventoryParams.alternateItems.add(alternateItemInstance)
                }

                // spareFor.
                tempSpareFor = parseInputList(inventoryParams.spareFor)
                inventoryParams.spareFor = []

                for(asset in tempSpareFor) {

                    asset = WordUtils.capitalize(asset)

                    spareForInstance = Asset.findByName(asset)
                    if(!spareForInstance) {
                        log.error "Failed to find 'Spare For' asset on line: " + lineNumber
                        return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                    }

                    inventoryParams.spareFor.add(spareForInstance)
                }

                // Assign the retrieved or created instances to params.
                inventoryParams.inventoryLocation = inventoryLocationInstance
                inventoryParams.inventoryGroup = inventoryGroupInstance
                inventoryParams.inventoryType = inventoryTypeInstance
                inventoryParams.unitOfMeasure = unitOfMeasureInstance
                inventoryParams.averageDeliveryPeriod = averageDeliveryPeriodInstance
                inventoryParams.preferredSupplier = preferredSupplierInstance
                inventoryParams.preferredManufacturer = preferredManufacturerInstance

                // Name.
                // Checked above for blank string.
                inventoryParams.name = WordUtils.capitalize(inventoryParams.name)

                // Description.
                if(inventoryParams.description != '')
                    inventoryParams.description = inventoryParams.description[0].toUpperCase() + inventoryParams.description[1..-1]

                // Debug
                log.debug "InventoryParams: "
                log.debug inventoryParams

                // Create new or update.
                inventoryItemInstance = InventoryItem.findByName(inventoryParams.name)
                if(inventoryItemInstance) {
                    log.info "Updating existing item: " + inventoryItemInstance
                    inventoryItemInstance.properties = inventoryParams
                }
                else {
                    log.info "Creating new item: " + inventoryParams.name
                    inventoryItemInstance = new InventoryItem(inventoryParams)
                }

                // Save inventoryItem.
                if(inventoryItemInstance.hasErrors() || !inventoryItemInstance.save()) {
                    log.error "Failed to create item on line: " + lineNumber
                    log.debug inventoryItemInstance.errors
                    return fail(code: "inventory.import.failure", args: [lineNumber, logFileLink])
                }

                if(lineNumber % 100 == 0)
                    cleanUpGorm()

                if(!result.error) nextLine()
            } //while(line)

            // Success.
            log.info "End of file."
            reader.close()
            return result

        } //end withTransaction
    } // end importInventory()

    /**
    * Build an inventory template csv file.
    * This template can then be populated for import.
    * @returns The template as a String in csv format.
    */
    def buildInventoryTemplate() {

        StringWriter sw = new StringWriter()
        CSVWriter writer = new CSVWriter(sw)

        writeTemplateLines(writer)

        writer.close()
        return sw.toString()
    }

    private writeTemplateLines(writer) {
        writer.writeNext(templateHeaderLine1 as String[])
        writer.writeNext()
        writer.writeNext("Comment: The header line is required.")
        writer.writeNext("Comment: Required columns are marked with a (*) in the header line.")
        writer.writeNext("Comment: Lists of items in a column must be separated by a semicolon (;), not a comma.")
        writer.writeNext("Comment: The at symbol (@) is reserved for indicating supplier and manufacturer types.")
        writer.writeNext("Comment: Identical and existing names will be considered as the same item.")
        writer.writeNext("Comment: Lines containing 'comment' will be ignored.")
        writer.writeNext("Comment: Lines containing 'example' will be ignored.")
        writer.writeNext("Comment: This file must be saved as a CSV file before import.")
        writer.writeNext()
    }

    /**
    * Build an inventory example/test file.
    * This test file can be imported to test the import and export methods.
    * @returns The test file as a String in csv format.
    */
    def buildInventoryExample() {

        StringWriter sw = new StringWriter()
        CSVWriter writer = new CSVWriter(sw)

        writeTemplateLines(writer)

        // Requires creation of some of the base/group/type data.
        writer.writeNext(["Split19", "19mm split pin", "Very usefull item.",
                                        "1024", "0", "1",
                                        "each", "5", "NZD",
                                        "false", "BR4",
                                        "Store #99", "Inventory Depot",
                                        "Mechanical Stock",
                                        "Consumable",
                                        "7", "Week(s)",
                                        "123", "Multi Supplier@Local",
                                        "Multi Distributors1@OEM; Multi Distributors2@Local",
                                        "321", "Master Manufacturer@OEM",
                                        "Mega Manufacturer1@OEM;Mega Manufacturer2@Alternate",
                                        "2204E-2RS", ""
                                        ] as String[])

        // Using existing base data.
        writer.writeNext(["2204E-2RS", "Double Row Self Align Ball Bearing 2204E-2RS - Sealed - 20/47x18", "",
                                        "4", "1", "9",
                                        "each", "16.35", "USD",
                                        "TRUE", "BR4",
                                        "Store #99", "Inventory Depot",
                                        "Mechanical Stock",
                                        "Consumable",
                                        "2", "Month(s)",
                                        "456KL", "Multi Supplier",
                                        "Multi Distributors1; Multi Distributors2",
                                        "654OP", "Master Manufacturer",
                                        "Mega Manufacturer1;Mega Manufacturer2",
                                        "", ""
                                        ] as String[])

        writer.close()
        return sw.toString()
    }

    /**
    * Build complete inventory for export.
    * @param inventoryItemList The list of inventory items to build.
    * @returns The inventory as a String in csv format.
    */
    def buildInventory(List inventoryItemList) {

        def sw = new StringWriter()
        def writer = new CSVWriter(sw)

        writeTemplateLines(writer)

        //Rows
        def row

        inventoryItemList.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }.each() { inventoryItem ->
            row = []
            row.add(inventoryItem.name)
            row.add(inventoryItem.description)
            row.add(inventoryItem.comment)
            row.add(inventoryItem.unitsInStock)
            row.add(inventoryItem.reorderPoint)
            row.add(inventoryItem.recommendedReorderPoint)
            row.add(inventoryItem.unitOfMeasure)
            row.add(inventoryItem.estimatedUnitPriceAmount)
            row.add(inventoryItem.estimatedUnitPriceCurrency)
            row.add(inventoryItem.enableReorder)
            row.add(inventoryItem.inventoryLocation)
            row.add(inventoryItem.inventoryLocation.inventoryStore)
            row.add(inventoryItem.inventoryLocation.inventoryStore.site)
            row.add(inventoryItem.inventoryGroup)
            row.add(inventoryItem.inventoryType)
            row.add(inventoryItem.averageDeliveryTime)
            row.add(inventoryItem.averageDeliveryPeriod)
            row.add(inventoryItem.suppliersPartNumber)

            if(inventoryItem.preferredSupplier)
                row.add( inventoryItem.preferredSupplier.name + "@" + inventoryItem.preferredSupplier.supplierType )
            else
                row.add('')

            row.add( inventoryItem.alternateSuppliers.sort { p1, p2 ->
                p1.name.compareToIgnoreCase(p2.name)
            }.collect { it.name + "@" + it.supplierType }.join(';') )

            row.add(inventoryItem.manufacturersPartNumber)

            if(inventoryItem.preferredManufacturer)
                row.add( inventoryItem.preferredManufacturer.name + "@" + inventoryItem.preferredManufacturer.manufacturerType )
            else
                row.add('')

            row.add(inventoryItem.alternateManufacturers.sort { p1, p2 ->
                p1.name.compareToIgnoreCase(p2.name)
            }.collect { it.name + "@" + it.manufacturerType }.join(';'))

            row.add(inventoryItem.alternateItems.sort { p1, p2 ->
                p1.name.compareToIgnoreCase(p2.name)
            }.collect { it.name }.join(';') )

            row.add(inventoryItem.spareFor.sort { p1, p2 ->
                p1.name.compareToIgnoreCase(p2.name)
            }.collect { it.name }.join(';'))

            writer.writeNext(row as String[])
        }

        writer.close()
        return sw.toString()
    } // end buildInventory()

    /**
    * Import inventoryItemPurchases creating items as required.
    */
    def importInventoryItemPurchases(request) {
        InventoryItemPurchase.withTransaction { status ->
            def result = [:]

            def kByteMultiplier = 1000
            def fileMaxSize = 800 * kByteMultiplier
            def logFileLink = g.link(controller: "appCore", action: "appLog") {"log"}

            def multiPartFile = request.getFile('file')

            InputStreamReader sr = new InputStreamReader(multiPartFile.inputStream)
            CSVReader reader = new CSVReader(sr)

            def fail = { Map m ->
                status.setRollbackOnly()
                reader.close()
                result.error = [ code: m.code, args: m.args ]
                return result
            }

            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/kByteMultiplier, "kB"])

            def line = []
            def lineNumber = 0
            def maxNumberOfColumns = 10
            def inventoryItemPurchaseParams = [:]
            def inventoryItemPurchaseProperties = ["inventoryItem", "purchaseOrderNumber", "quantity",
                                                                                "inventoryItemPurchaseType",
                                                                                "costCode", "enteredBy", "dateEntered",
                                                                                "orderValueAmount", "orderValueCurrency", "invoiceNumber"]

            def personInstance
            def costCodeInstance
            def inventoryItemInstance
            def inventoryItemPurchaseInstance
            def inventoryItemPurchaseTypeInstance

            def nextLine = {
                    line = reader.readNext()
                    lineNumber ++
                    log.info "Processing line: " + lineNumber
            }

            def parseInputDate = {
                if( (it == null) || (it.trim() == '') ) {
                    log.error "Failed to find any date on line: " + lineNumber
                    return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                }

                def d = it.split("/").collect{it.trim()}
                if(d.size() != 3) {
                    log.error "Failed to find full date on line: " + lineNumber
                    return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                }
                dateUtilService.makeDate(d[0], d[1], d[2])
            }

            // Get first line.
            nextLine()

            // Check for header line 1.
            if(line != purchasesTemplateHeaderLine1) {
                log.error "Failed to find header line 1. "
                log.error "Required: " + purchasesTemplateHeaderLine1.toString()
                log.error "Supplied: " + line.toString()
                return fail(code: "default.file.no.header")
            }

            log.info "Header line found."

            // Prepare the first body line.
            nextLine()

            // Primary loop.
            while(line) {

                if(line.size() > maxNumberOfColumns) {
                    log.error "Too many columns on line: " + lineNumber
                    return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                }

                // Ignore comment lines.
                if(line.toString().toLowerCase().contains("comment")) {
                    log.info "Comment line found."
                    nextLine()
                    continue
                }

                // Ignore example lines.
                if(line.toString().toLowerCase().contains("example")) {
                    log.info "Example line found."
                    nextLine()
                    continue
                }

                // Parse the line into the params map.
                inventoryItemPurchaseParams = [:]
                line.eachWithIndex { it, j ->
                    inventoryItemPurchaseParams."${inventoryItemPurchaseProperties[j]}" = it.trim()
                }

                // Debug
                log.debug " Supplied params: "
                log.debug inventoryItemPurchaseParams

                // Ignore blank lines.
                if(inventoryItemPurchaseParams.inventoryItem == '') {
                    log.info "No inventory item name found."
                    nextLine()
                    continue
                }

                // Inventory Item.
                inventoryItemPurchaseParams.inventoryItem = WordUtils.capitalize(inventoryItemPurchaseParams.inventoryItem)
                inventoryItemInstance = InventoryItem.findByName(inventoryItemPurchaseParams.inventoryItem)
                if(!inventoryItemInstance) {
                    log.error "Inventory item not found on line: " + lineNumber
                    return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                }
                inventoryItemPurchaseParams.inventoryItem = inventoryItemInstance

                // Quantity.
                if(inventoryItemPurchaseParams.quantity.isInteger())
                    inventoryItemPurchaseParams.quantity = inventoryItemPurchaseParams.quantity.toInteger()
                else {
                    log.error "Quantity is not a valid number on line: " + lineNumber
                    return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                }

                // InventoryItemPurchaseType.
                inventoryItemPurchaseParams.inventoryItemPurchaseType = WordUtils.capitalizeFully(inventoryItemPurchaseParams.inventoryItemPurchaseType)
                inventoryItemPurchaseTypeInstance = InventoryItemPurchaseType.findByName(inventoryItemPurchaseParams.inventoryItemPurchaseType)
                if(!inventoryItemPurchaseTypeInstance) {
                    log.error "Inventory item purchase type not found on line: " + lineNumber
                    log.debug inventoryItemPurchaseParams.inventoryItemPurchaseType
                    return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                }
                inventoryItemPurchaseParams.inventoryItemPurchaseType = inventoryItemPurchaseTypeInstance

                // CostCode.
                if(inventoryItemPurchaseParams.costCode != '') {
                    inventoryItemPurchaseParams.costCode = WordUtils.capitalizeFully(inventoryItemPurchaseParams.costCode)
                    costCodeInstance = CostCode.findByName(inventoryItemPurchaseParams.costCode)
                    if(!costCodeInstance) {
                        costCodeInstance = new CostCode(name: inventoryItemPurchaseParams.costCode)
                        if(!costCodeInstance.save()) {
                            log.error "Failed to create cost code on line: " + lineNumber
                            return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                        }
                    }
                    inventoryItemPurchaseParams.costCode = costCodeInstance
                }

                // Entered By.
                inventoryItemPurchaseParams.enteredBy = inventoryItemPurchaseParams.enteredBy.toLowerCase()
                personInstance = Person.findByLoginName(inventoryItemPurchaseParams.enteredBy)
                if(!personInstance) {
                    log.error "Entered by person not found on line: " + lineNumber
                    return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                }
                inventoryItemPurchaseParams.enteredBy = personInstance

                // Date Entered.
                inventoryItemPurchaseParams.dateEntered = parseInputDate(inventoryItemPurchaseParams.dateEntered)

                // Debug
                log.debug "InventoryItemPurchaseParams: "
                log.debug inventoryItemPurchaseParams

                // Save inventoryItem.
                log.info "Creating new purchase."
                inventoryItemPurchaseInstance = new InventoryItemPurchase(inventoryItemPurchaseParams)

                if(inventoryItemPurchaseInstance.hasErrors() || !inventoryItemPurchaseInstance.save()) {
                    log.error "Failed to create item on line: " + lineNumber
                    log.debug inventoryItemPurchaseInstance.errors
                    return fail(code: "inventoryItemPurchase.import.failure", args: [lineNumber, logFileLink])
                }

                if(lineNumber % 100 == 0)
                    cleanUpGorm()

                if(!result.error) nextLine()
            } //while(line)

            // Success.
            log.info "End of file."
            reader.close()
            return result


         } //end withTransaction
    } // end importInventoryItemPurchases()

    private getTemplateHeaderLine1() {
            ["Name*", "Description", "Comment", "Units In Stock", "Reorder Point*", "Recommended Reorder Point", "Unit Of Measure*",
            "Estimated Unit Price", "Currency", "Enable Reorder", "Location*", "Store*", "Site*", "Group*", "Type*",
            "Average Delivery Time", "Average Delivery Period", "Supplier's Part Number", "Preferred Supplier", "Alternate Suppliers",
            "Manufacturer's Part Number", "Preferred Manufacturer", "Alternate Manufacturers", "Alternate Item", "Spare For"]
    }

    private getPurchasesTemplateHeaderLine1() {
            ["Inventory Item*", "Purchase Order Number*", "Quantity*", "Purchase Type*", "Cost Code*", "Entered By*",
            "Date Entered*", "Order Value", "Currency", "Invoice Number"]
    }

    /**
    * 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