/*
 * Copyright 2000-2017 JetBrains s.r.o.
 *
 * Licensed 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 com.jetbrains.reflection

import com.intellij.util.containers.HashMap
import com.intellij.util.containers.SoftHashMap
import java.beans.Introspector
import java.beans.PropertyDescriptor
import java.lang.ref.SoftReference
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty
import kotlin.reflect.KProperty
import kotlin.reflect.jvm.javaType
import kotlin.reflect.memberProperties

/**
 * Tools to fetch properties both from Java and Kotlin code and to copy them from one object to another.
 * To be a property container class should have kotlin properties, java bean properties or implement [SimplePropertiesProvider].
 *
 * Properties should be writable except [DelegationProperty]
 * @author Ilya.Kazakevich
 */

interface Property {
  fun getName(): String
  fun getType(): java.lang.reflect.Type
  fun get(): Any?
  fun set(value: Any?)
}

private class KotlinProperty(val property: KMutableProperty<*>, val instance: Any?) : Property {
  override fun getName() = property.name
  override fun getType() = property.returnType.javaType
  override fun get() = property.getter.call(instance)
  override fun set(value: Any?) = property.setter.call(instance, value)
}

private class JavaProperty(val property: PropertyDescriptor, val instance: Any?) : Property {
  override fun getName() = property.name!!
  override fun getType() = property.propertyType!!
  override fun get() = property.readMethod.invoke(instance)!!
  override fun set(value: Any?) {
    property.writeMethod.invoke(instance, value)
  }
}

private class SimpleProperty(private val propertyName: String,
                             private val provider: SimplePropertiesProvider) : Property {
  override fun getName() = propertyName
  override fun getType() = String::class.java
  override fun get() = provider.getPropertyValue(propertyName)
  override fun set(value: Any?) = provider.setPropertyValue(propertyName, value?.toString())
}

/**
 * Implement to handle properties manually
 */
interface SimplePropertiesProvider {
  val propertyNames: List<String>
  fun setPropertyValue(propertyName: String, propertyValue: String?)
  fun getPropertyValue(propertyName: String): String?
}

class Properties(val properties: List<Property>, val instance: Any) {
  val propertiesMap: MutableMap<String, Property> = HashMap(properties.map { Pair(it.getName(), it) }.toMap())

  init {
    if (instance is SimplePropertiesProvider) {
      instance.propertyNames.forEach { propertiesMap.put(it, SimpleProperty(it, instance)) }
    }
  }

  fun copyTo(dst: Properties) {
    propertiesMap.values.forEach {
      val dstProperty = dst.propertiesMap[it.getName()]
      if (dstProperty != null) {
        val value = it.get()
        dstProperty.set(value)
      }
    }
  }
}


private fun KProperty<*>.isAnnotated(annotation: KClass<*>): Boolean {
  return this.annotations.find { annotation.java.isAssignableFrom(it.javaClass) } != null
}


private val membersCache: MutableMap<KClass<*>, SoftReference<Collection<KProperty<*>>>> = SoftHashMap()

private fun KClass<*>.memberPropertiesCached(): Collection<KProperty<*>> {
  synchronized(membersCache) {
    val cache = membersCache[this]?.get()
    if (cache != null) {
      return cache
    }
    val memberProperties = this.memberProperties
    membersCache.put(this, SoftReference(memberProperties))
    return memberProperties
  }
}

/**
 * @param instance object with properties (see module doc)
 * @param annotationToFilterByClass optional annotation class to fetch only kotlin properties annotated with it. Only supported in Kotlin
 * @param usePojoProperties search for java-style properties (kotlin otherwise)
 * @return properties of some object
 */
fun getProperties(instance: Any, annotationToFilterByClass: Class<*>? = null, usePojoProperties: Boolean = false): Properties {
  val annotationToFilterBy = annotationToFilterByClass?.kotlin

  if (usePojoProperties) {
    // Java props
    val javaProperties = Introspector.getBeanInfo(instance.javaClass).propertyDescriptors
    assert(annotationToFilterBy == null, { "Filtering java properties is not supported" })
    return Properties(javaProperties.map { JavaProperty(it, instance) }, instance)
  }
  else {
    // Kotlin props
    val klass = instance.javaClass.kotlin
    val allKotlinProperties = LinkedHashSet(klass.memberPropertiesCached().filterIsInstance(KProperty::class.java))

    val delegatedProperties = ArrayList<Property>() // See DelegationProperty doc
    allKotlinProperties.filter { it.isAnnotated(DelegationProperty::class) }.forEach {
      val delegatedInstance = it.getter.call(instance)
      if (delegatedInstance != null) {
        delegatedProperties.addAll(getProperties(delegatedInstance, annotationToFilterBy?.java, false).properties)
        allKotlinProperties.remove(it)
      }
    }
    val firstLevelProperties = allKotlinProperties.filterIsInstance(KMutableProperty::class.java)

    if (annotationToFilterBy == null) {
      return Properties(firstLevelProperties.map { KotlinProperty(it, instance) } + delegatedProperties, instance)
    }
    return Properties(
      firstLevelProperties.filter { it.isAnnotated(annotationToFilterBy) }.map { KotlinProperty(it, instance) } + delegatedProperties, instance)
  }
}


@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.PROPERTY)
/**
 * Property marked with it is not considered to be [Property] by itself, but class with properties instead.
 * Following structure is example:
 * class User:
 *  +familyName: String
 *  +lastName: String
 *  +credentials: Credentials
 *
 * class Credentials:
 *  +login: String
 *  +password: String
 *
 * Property credentials here is [DelegationProperty]. It can be val, but all other properties should be var
 */
annotation class DelegationProperty