/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.jackrabbit.oak.console.commands

import com.google.common.base.Stopwatch
import org.apache.jackrabbit.oak.commons.PathUtils
import org.apache.jackrabbit.oak.console.ConsoleSession
import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexDefinition
import org.apache.jackrabbit.oak.plugins.index.lucene.directory.OakDirectory
import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants
import org.apache.jackrabbit.oak.spi.commit.CommitInfo
import org.apache.jackrabbit.oak.spi.commit.EmptyHook
import org.apache.jackrabbit.oak.spi.state.NodeBuilder
import org.apache.jackrabbit.oak.spi.state.NodeState
import org.apache.jackrabbit.oak.spi.state.NodeStore
import org.apache.jackrabbit.oak.spi.state.ReadOnlyBuilder
import org.apache.lucene.index.DirectoryReader
import org.apache.lucene.store.Directory
import org.apache.lucene.store.FSDirectory
import org.apache.lucene.store.IOContext
import org.codehaus.groovy.tools.shell.ComplexCommandSupport
import org.codehaus.groovy.tools.shell.Groovysh
import org.apache.jackrabbit.oak.spi.state.NodeBuilder as OakNodeBuilder


import static com.google.common.base.Preconditions.checkNotNull

class LuceneCommand extends ComplexCommandSupport {
    public static final String COMMAND_NAME = 'lucene'

    public LuceneCommand(final Groovysh shell) {
        super(shell, COMMAND_NAME, 'lc', ['info', 'rmdata', 'dump'], 'info')
    }

    def do_info = { args ->
        String idxPath = args ? args[0] : '/oak:index/lucene'
        Directory dir = getDirectory(idxPath)

        if (!dir) {
            io.out.println("No Lucene directory found at path [$idxPath]")
            return
        }
        try {
            io.out.println("Index size : ${humanReadableByteCount(dirSize(dir))}")
            DirectoryReader reader = DirectoryReader.open(dir)
            try {
                io.out.println("Number of documents : ${reader.numDocs()}")
                io.out.println("Number of deleted documents : ${reader.numDeletedDocs()}")
            }finally{
                reader.close()
            }
        } finally {
            dir.close()
        }
    }

    def do_dump = { args ->
        String idxPath = args && args.size() == 2 ? args[1] : '/oak:index/lucene'
        String destPath = args ? args[0] : 'luceneIndex'
        Directory source = getDirectory(idxPath)
        if (!source) {
            io.out.println("No Lucene directory found at path [$idxPath]")
            return
        }
        try {
            File destDir = new File(destPath)
            Stopwatch w = Stopwatch.createStarted()
            io.out.println("Copying Lucene indexes to [${destDir.absolutePath}]")
            Directory dest = FSDirectory.open(destDir)
            long size = 0
            source.listAll().each { file ->
                size += source.fileLength(file)
                source.copy(dest, file, file, IOContext.DEFAULT)
            }
            io.out.println("Copied ${humanReadableByteCount(size)} in $w")
        } finally {
            source.close()
        }
    }

    def do_rmdata = {args ->
        if (!args || args.size() != 1) {
            io.out.println("Please provide a single argument (index-path). Current arg list: ${args}")
            return
        }

        String idxPath = args[0]

        NodeStore ns = getSession().getStore();
        OakNodeBuilder nb = ns.root.builder()
        String dataPath = idxPath + "/:data"
        NodeBuilder data = getNode(nb, dataPath)

        if (!data.exists()){
            io.out.println("Index at [$idxPath] is empty. Nothing to remove")
            return
        }

        data.remove()
        ns.merge(nb, EmptyHook.INSTANCE, CommitInfo.EMPTY)

        io.out.println("Removed the index data for [$idxPath]")
    }

    private Directory getDirectory(String path) {
        NodeState definition = variables.session.getRoot();
        for (String element : PathUtils.elements(path)) {
            definition = definition.getChildNode(element);
        }
        NodeState data = definition.getChildNode(FulltextIndexConstants.INDEX_DATA_CHILD_NAME);
        if (data.exists()) {
            //OakDirectory is package scope but Groovy allows us
            //to use it. Good or bad but its helpful debug scripts
            //can access inner classes and prod code cannot. Win win :)
            return new OakDirectory(new ReadOnlyBuilder(definition), new LuceneIndexDefinition(session.root, definition, path), true);
        }
        return null
    }

    private static long dirSize(Directory dir) {
        dir.listAll().inject(0L, { sum, fileName -> sum + dir.fileLength(fileName) })
    }

    private static def humanReadableByteCount(long bytes) {
        if(bytes == 0){
            return ""
        }
        int exp = (int) (Math.log(bytes) / Math.log(1024));
        if (exp == 0) {
            return String.format("%d bytes", bytes);
        } else {
            String pre = "KMGTPE".charAt(exp - 1);
            return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
        }
    }

    private ConsoleSession getSession(){
        return (ConsoleSession)variables.session
    }

    private static NodeBuilder getNode(NodeBuilder node, String path) {
        for (String name : PathUtils.elements(checkNotNull(path))) {
            node = node.getChildNode(checkNotNull(name));
        }
        return node;
    }
}
